Checkpoint: pathology panel modal workflow, gateway component search, and ABDM-style component/print editors

This commit is contained in:
DevSoftTech Server 2026-05-30 06:32:30 +05:30
parent e449af8137
commit 78c23af7e5
21 changed files with 5424 additions and 6159 deletions

View File

@ -65,11 +65,28 @@ class ExportLabTemplates extends BaseCommand
orderBy: 'id ASC',
);
// Include loinc_code only if the column exists (migration may not have run on all environments)
$repoFields = $this->db->getFieldNames('lab_repo') ?? [];
$testFields = $this->db->getFieldNames('lab_tests') ?? [];
$labRepoCols = ['mstRepoKey', 'Title', 'RTFData', 'HTMLData', 'GrpKey',
'IncludeHeader', 'IncludeFooter', 'charge_id'];
if (in_array('loinc_code', $repoFields, true)) {
$labRepoCols[] = 'loinc_code';
}
$labTestsCols = ['mstTestKey', 'Test', 'TestID', 'Result', 'Options', 'Formula',
'VRule', 'VMsg', 'Unit', 'FixedNormals', 'isGenderSpecific', 'FixedNormalsWomen'];
foreach (['loinc_code', 'loinc_property', 'loinc_system', 'loinc_scale'] as $col) {
if (in_array($col, $testFields, true)) {
$labTestsCols[] = $col;
}
}
$this->exportTable(
filename: '03_lab_repo.sql',
table: 'lab_repo',
columns: ['mstRepoKey', 'Title', 'RTFData', 'HTMLData', 'GrpKey',
'IncludeHeader', 'IncludeFooter', 'charge_id'],
columns: $labRepoCols,
schema: $this->schemaLabRepo(),
orderBy: 'mstRepoKey ASC',
);
@ -77,8 +94,7 @@ class ExportLabTemplates extends BaseCommand
$this->exportTable(
filename: '04_lab_tests.sql',
table: 'lab_tests',
columns: ['mstTestKey', 'Test', 'TestID', 'Result', 'Options', 'Formula',
'VRule', 'VMsg', 'Unit', 'FixedNormals', 'isGenderSpecific', 'FixedNormalsWomen'],
columns: $labTestsCols,
schema: $this->schemaLabTests(),
orderBy: 'mstTestKey ASC',
);
@ -244,6 +260,8 @@ CREATE TABLE IF NOT EXISTS `lab_repo` (
`IncludeHeader` tinyint(1) DEFAULT '1',
`IncludeFooter` tinyint(1) DEFAULT '1',
`charge_id` int DEFAULT '0',
`loinc_code` varchar(20) DEFAULT NULL,
`loinc_synced_at` datetime DEFAULT NULL,
PRIMARY KEY (`mstRepoKey`),
UNIQUE KEY `Title` (`Title`),
KEY `GrpKey` (`GrpKey`),
@ -268,9 +286,15 @@ CREATE TABLE IF NOT EXISTS `lab_tests` (
`FixedNormals` varchar(30) NOT NULL DEFAULT '0',
`isGenderSpecific` int NOT NULL DEFAULT '0',
`FixedNormalsWomen` varchar(30) NOT NULL,
`loinc_code` varchar(20) DEFAULT NULL,
`loinc_property` varchar(50) DEFAULT NULL,
`loinc_system` varchar(50) DEFAULT NULL,
`loinc_scale` varchar(20) DEFAULT NULL,
`loinc_synced_at` datetime DEFAULT NULL,
PRIMARY KEY (`mstTestKey`),
UNIQUE KEY `TestID` (`TestID`),
KEY `Test` (`Test`)
KEY `Test` (`Test`),
KEY `idx_lab_tests_loinc` (`loinc_code`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb3;
SQL;
}

View File

@ -0,0 +1,403 @@
<?php
namespace App\Commands;
use App\Libraries\Abdm\EAtriaBridgeConnector;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
/**
* Spark command: abdm:sync-pathology-loinc
*
* Fetches LOINC codes from the Bridge API pathology terminology endpoints
* and maps them to the local lab_repo (panel) and lab_tests (component) tables.
*
* Matching strategy:
* 1. For lab_repo: case-insensitive exact match on Title vs bridge test_name,
* then normalised fuzzy match (strip spaces/punctuation, compare words).
* 2. For lab_tests: same matching logic on Test vs component_test_name,
* scoped to the matched panel.
*
* Usage:
* php spark abdm:sync-pathology-loinc
* php spark abdm:sync-pathology-loinc --dry-run
* php spark abdm:sync-pathology-loinc --since=2025-01-01T00:00:00Z
* php spark abdm:sync-pathology-loinc --category=PATHOLOGY
* php spark abdm:sync-pathology-loinc --limit=500
*/
class SyncPathologyLoinc extends BaseCommand
{
protected $group = 'Bridge';
protected $name = 'abdm:sync-pathology-loinc';
protected $description = 'Sync LOINC codes from Bridge API into lab_tests and lab_repo tables.';
protected $usage = 'abdm:sync-pathology-loinc [options]';
protected $arguments = [];
protected $options = [
'--dry-run' => 'Print matches without updating the database.',
'--since' => 'ISO-8601 datetime for incremental sync (e.g. 2025-01-01T00:00:00Z).',
'--category' => 'Filter bridge masters by sub_category (PATHOLOGY|BIOPSY). Default: all.',
'--limit' => 'Items per API page request. Default: 200.',
];
/** @var \CodeIgniter\Database\BaseConnection */
private $db;
/** @var bool */
private bool $dryRun = false;
/** @var int */
private int $masterMatched = 0;
private int $masterSkipped = 0;
private int $compMatched = 0;
private int $compSkipped = 0;
public function run(array $params): void
{
$this->dryRun = (bool) CLI::getOption('dry-run');
$since = (string) (CLI::getOption('since') ?? '');
$category = (string) (CLI::getOption('category') ?? '');
$limit = (int) (CLI::getOption('limit') ?? 200);
if ($limit <= 0) {
$limit = 200;
}
$this->db = \Config\Database::connect();
if ($this->dryRun) {
CLI::write('[DRY-RUN] No database changes will be made.', 'yellow');
}
CLI::write('=== Syncing Bridge Pathology LOINC Codes ===', 'cyan');
try {
$connector = new EAtriaBridgeConnector();
} catch (\Throwable $e) {
CLI::error('Failed to initialise Bridge connector: ' . $e->getMessage());
return;
}
// Load local tables once
$localRepos = $this->loadLocalRepos();
$localTests = $this->loadLocalTests();
CLI::write('Local lab_repo records : ' . count($localRepos));
CLI::write('Local lab_tests records: ' . count($localTests));
CLI::write('');
// --- Step 1: Fetch all panel masters and match to lab_repo ---
CLI::write('--- Phase 1: Panel Masters → lab_repo ---', 'yellow');
$masters = $this->fetchAllPages(
$connector,
'masters',
['category' => $category, 'since' => $since, 'limit' => $limit]
);
CLI::write('Bridge masters fetched: ' . count($masters));
// Build lookup: normalised name → master item
$masterByName = [];
foreach ($masters as $item) {
$key = $this->normalise((string) ($item['test_name'] ?? ''));
if ($key !== '') {
$masterByName[$key] = $item;
}
}
foreach ($localRepos as $repo) {
$this->matchAndUpdateRepo($repo, $masterByName);
}
CLI::write("Panel matches: {$this->masterMatched} | skipped (no match): {$this->masterSkipped}");
CLI::write('');
// --- Step 2: For each matched panel, fetch components → lab_tests ---
CLI::write('--- Phase 2: Components → lab_tests ---', 'yellow');
// Group local tests by repo id for fast lookup
$testsByRepo = [];
foreach ($localTests as $test) {
$rid = (int) ($test['mstRepoKey'] ?? 0);
$testsByRepo[$rid][] = $test;
}
foreach ($localRepos as $repo) {
$panelName = (string) ($repo['Title'] ?? '');
if ($panelName === '') {
continue;
}
$components = $this->fetchAllPages(
$connector,
'components',
['parent_test' => $panelName, 'since' => $since, 'limit' => $limit]
);
if (empty($components)) {
// Try with the bridge display_name if we synced one
$bridgeName = (string) ($repo['_bridge_test_name'] ?? '');
if ($bridgeName !== '' && $bridgeName !== $panelName) {
$components = $this->fetchAllPages(
$connector,
'components',
['parent_test' => $bridgeName, 'since' => $since, 'limit' => $limit]
);
}
}
if (empty($components)) {
continue;
}
$repoTests = $testsByRepo[(int) $repo['mstRepoKey']] ?? [];
if (empty($repoTests)) {
continue;
}
// Build lookup by normalised component name
$compByName = [];
foreach ($components as $comp) {
$key = $this->normalise((string) ($comp['component_test_name'] ?? ''));
if ($key !== '') {
$compByName[$key] = $comp;
}
}
foreach ($repoTests as $test) {
$this->matchAndUpdateTest($test, $compByName);
}
}
CLI::write("Component matches: {$this->compMatched} | skipped (no match): {$this->compSkipped}");
CLI::write('');
CLI::write('Done.', 'green');
}
// -------------------------------------------------------------------------
// Match + Update helpers
// -------------------------------------------------------------------------
/** @param array<string, mixed> $repo */
private function matchAndUpdateRepo(array $repo, array $masterByName): void
{
$title = (string) ($repo['Title'] ?? '');
if ($title === '') {
return;
}
$key = $this->normalise($title);
$item = $masterByName[$key] ?? null;
// Fuzzy fallback: try partial word match
if ($item === null) {
$item = $this->fuzzyMatch($title, $masterByName);
}
if ($item === null) {
$this->masterSkipped++;
return;
}
$loincCode = (string) ($item['code'] ?? '');
$bridgeTestName = (string) ($item['test_name'] ?? '');
// Stash bridge name so phase 2 can reuse it
$repo['_bridge_test_name'] = $bridgeTestName;
if ($this->dryRun) {
CLI::write(" [DRY] lab_repo #{$repo['mstRepoKey']} '{$title}' → LOINC: {$loincCode} (bridge: '{$bridgeTestName}')");
} else {
$this->db->table('lab_repo')->where('mstRepoKey', $repo['mstRepoKey'])->update([
'loinc_code' => $loincCode,
'loinc_synced_at' => date('Y-m-d H:i:s'),
]);
}
$this->masterMatched++;
}
/** @param array<string, mixed> $test */
private function matchAndUpdateTest(array $test, array $compByName): void
{
$testName = (string) ($test['Test'] ?? '');
if ($testName === '') {
return;
}
$key = $this->normalise($testName);
$comp = $compByName[$key] ?? null;
if ($comp === null) {
$comp = $this->fuzzyMatch($testName, $compByName);
}
if ($comp === null) {
$this->compSkipped++;
return;
}
$loincCode = (string) ($comp['component_code'] ?? '');
$loincProperty = (string) ($comp['component_property'] ?? '');
$loincSystem = (string) ($comp['component_system'] ?? '');
$loincScale = (string) ($comp['component_scale'] ?? '');
if ($this->dryRun) {
CLI::write(
" [DRY] lab_tests #{$test['mstTestKey']} '{$testName}' → " .
"LOINC: {$loincCode} | prop: {$loincProperty} | sys: {$loincSystem} | scale: {$loincScale}"
);
} else {
$this->db->table('lab_tests')->where('mstTestKey', $test['mstTestKey'])->update([
'loinc_code' => $loincCode,
'loinc_property' => $loincProperty,
'loinc_system' => $loincSystem,
'loinc_scale' => $loincScale,
'loinc_synced_at' => date('Y-m-d H:i:s'),
]);
}
$this->compMatched++;
}
// -------------------------------------------------------------------------
// API pagination
// -------------------------------------------------------------------------
/**
* Fetch all pages from the Bridge API for either 'masters' or 'components'.
*
* @param array<string, mixed> $opts
* @return array<int, array<string, mixed>>
*/
private function fetchAllPages(EAtriaBridgeConnector $connector, string $type, array $opts): array
{
$limit = (int) ($opts['limit'] ?? 200);
$since = (string) ($opts['since'] ?? '');
$all = [];
$offset = 0;
do {
try {
if ($type === 'masters') {
$resp = $connector->pathologyMasters(
(string) ($opts['category'] ?? ''),
$limit,
$offset,
$since
);
} else {
$resp = $connector->pathologyComponents(
(string) ($opts['parent_test'] ?? ''),
$limit,
$offset,
$since
);
}
} catch (\Throwable $e) {
CLI::error("API error ({$type}, offset={$offset}): " . $e->getMessage());
break;
}
if (! empty($resp['error']) || (isset($resp['success']) && $resp['success'] === false)) {
CLI::write(' API error response: ' . json_encode($resp), 'red');
break;
}
$items = $resp['items'] ?? $resp['data'] ?? [];
if (! is_array($items) || $items === []) {
break;
}
$all = array_merge($all, $items);
$offset += count($items);
// If fewer items returned than limit, we've reached the end
if (count($items) < $limit) {
break;
}
} while (true);
return $all;
}
// -------------------------------------------------------------------------
// Local DB loaders
// -------------------------------------------------------------------------
/** @return array<int, array<string, mixed>> */
private function loadLocalRepos(): array
{
return $this->db->table('lab_repo')
->select('mstRepoKey, Title, loinc_code')
->get()
->getResultArray();
}
/** @return array<int, array<string, mixed>> */
private function loadLocalTests(): array
{
// Join via lab_repotests to know which repo each test belongs to.
// A test may appear in multiple repos; we update it once (last-wins is fine).
return $this->db->table('lab_tests lt')
->select('lt.mstTestKey, lt.Test, lt.loinc_code, lrt.mstRepoKey')
->join('lab_repotests lrt', 'lrt.mstTestKey = lt.mstTestKey', 'left')
->get()
->getResultArray();
}
// -------------------------------------------------------------------------
// Name matching helpers
// -------------------------------------------------------------------------
private function normalise(string $name): string
{
$name = strtolower($name);
// Remove parenthetical suffixes like "(CBC)" or "[Panel]"
$name = preg_replace('/[\(\[\{][^\)\]\}]*[\)\]\}]/', '', $name);
// Strip punctuation except hyphen
$name = preg_replace('/[^a-z0-9\- ]/', '', $name);
// Collapse multiple spaces
$name = preg_replace('/\s+/', ' ', $name);
return trim($name);
}
/**
* Fuzzy match: find the entry in $candidates whose normalised key shares
* the highest proportion of words with the $name.
*
* @param array<string, array<string, mixed>> $candidates [normalisedKey => item]
* @return array<string, mixed>|null
*/
private function fuzzyMatch(string $name, array $candidates): ?array
{
$words = array_filter(explode(' ', $this->normalise($name)));
if (empty($words)) {
return null;
}
$best = null;
$bestScore = 0.0;
foreach ($candidates as $key => $item) {
$keyWords = array_filter(explode(' ', $key));
if (empty($keyWords)) {
continue;
}
$common = count(array_intersect($words, $keyWords));
$total = count(array_unique(array_merge($words, $keyWords)));
$score = $total > 0 ? $common / $total : 0.0;
if ($score > $bestScore) {
$bestScore = $score;
$best = $item;
}
}
// Require at least 50% Jaccard similarity
return $bestScore >= 0.5 ? $best : null;
}
}

View File

@ -825,12 +825,24 @@ $routes->get('Lab_Admin/print_pdf_create/(:num)/(:num)/(:num)', 'Diagnosis::prin
$routes->get('Lab_Admin/print_pdf_create/(:num)/(:num)/(:num)/(:num)', 'Diagnosis::printPdfCreate/$1/$2/$3/$4');
$routes->post('Lab_Admin/report_update', 'Setting\Template::report_update');
$routes->post('Lab_Admin/report_insert', 'Setting\Template::report_insert');
$routes->post('Lab_Admin/report_delete/(:num)', 'Setting\Template::report_delete/$1');
$routes->post('Lab_Admin/report_ultrasound_insert/(:num)', 'Setting\Template::report_ultrasound_insert/$1');
$routes->post('Lab_Admin/report_ultrasound_update/(:num)', 'Setting\Template::report_ultrasound_update/$1');
$routes->post('Lab_Admin/add_test_repo/(:num)/(:num)', 'Setting\Template::add_test_repo/$1/$2');
$routes->post('Lab_Admin/remove_test_item/(:num)/(:num)', 'Setting\Template::remove_test_item/$1/$2');
$routes->post('Lab_Admin/test_item_search', 'Setting\Template::test_item_search');
// LOINC Mapping Admin
$routes->get('Lab_Admin/loinc_panel', 'Setting\Template::loincPanel');
$routes->post('Lab_Admin/loinc_sync', 'Setting\Template::loincSync');
$routes->post('Lab_Admin/loinc_update', 'Setting\Template::loincUpdate');
$routes->get('Lab_Admin/pathology_masters_search', 'Setting\Template::pathologyMastersSearch');
$routes->get('Lab_Admin/pathology_component_masters_search', 'Setting\Template::pathologyComponentMastersSearch');
$routes->get('Lab_Admin/pathology_master_template', 'Setting\Template::pathologyMasterTemplate');
$routes->post('Lab_Admin/pathology_master_apply_panel', 'Setting\Template::pathologyMasterApplyPanel');
$routes->post('Lab_Admin/pathology_master_add_component', 'Setting\Template::pathologyMasterAddComponent');
$routes->get('Lab_Admin/pathology_bridge_debug', 'Setting\Template::pathologyBridgeDebug');
$routes->match(['get', 'post'], 'setting/template/discharge_templates', 'Setting\Template::discharge_templates');
$routes->get('setting/template/discharge_templates/delete/(:num)', 'Setting\Template::discharge_template_delete/$1');
$routes->match(['get', 'post'], 'setting/template/ipd_document_templates', 'Setting\Template::ipd_document_templates');

View File

@ -144,15 +144,39 @@ class AbdmGateway extends BaseController
]);
$queueId = null;
$gatewayConsentId = '';
try {
$result = $this->connector->requestConsent($patientId, $abhaId, $purposeCode, $expiresAt, $consentHandle, $rawPayload);
$queueId = $result['queue_id'] ?? null;
$gatewayConsentId = trim((string) ($result['gateway_consent_id'] ?? $result['consent_id'] ?? ''));
if ($gatewayConsentId !== '') {
$tableFields = $this->db->getFieldNames('abdm_consent_records') ?? [];
$updateData = [
'updated_at' => Time::now('Asia/Kolkata')->toDateTimeString(),
];
// Persist gateway consent id when a compatible column exists.
if (in_array('consent_id', $tableFields, true)) {
$updateData['consent_id'] = $gatewayConsentId;
} elseif (in_array('gateway_consent_id', $tableFields, true)) {
$updateData['gateway_consent_id'] = $gatewayConsentId;
}
$rawPayload['gateway_consent_id'] = $gatewayConsentId;
$updateData['raw_payload_json'] = (string) json_encode($rawPayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$this->db->table('abdm_consent_records')
->where('consent_handle', $consentHandle)
->update($updateData);
}
} catch (\Throwable $e) {
}
return $this->response->setJSON([
'ok' => 1,
'consent_handle' => $consentHandle,
'gateway_consent_id' => $gatewayConsentId !== '' ? $gatewayConsentId : null,
'queue_id' => $queueId,
'status' => 'requested',
]);
@ -177,6 +201,7 @@ class AbdmGateway extends BaseController
$handle = trim((string) $payload['consent_handle']);
$status = trim((string) ($payload['consent_status'] ?? 'approved'));
$expiresAt = trim((string) ($payload['expires_at'] ?? ''));
$incomingConsentId = trim((string) ($payload['consent_id'] ?? $payload['gateway_consent_id'] ?? ''));
$now = Time::now('Asia/Kolkata')->toDateTimeString();
$existing = $this->db->table('abdm_consent_records')
@ -229,11 +254,25 @@ class AbdmGateway extends BaseController
'updated_at' => $now,
];
if ($incomingConsentId !== '') {
$tableFields = $this->db->getFieldNames('abdm_consent_records') ?? [];
if (in_array('consent_id', $tableFields, true)) {
$update['consent_id'] = $incomingConsentId;
} elseif (in_array('gateway_consent_id', $tableFields, true)) {
$update['gateway_consent_id'] = $incomingConsentId;
}
}
$this->db->table('abdm_consent_records')
->where('consent_handle', $handle)
->update($update);
return $this->response->setJSON(['ok' => 1, 'consent_handle' => $handle, 'status' => $incomingStatus]);
return $this->response->setJSON([
'ok' => 1,
'consent_handle' => $handle,
'status' => $incomingStatus,
'gateway_consent_id' => $incomingConsentId !== '' ? $incomingConsentId : null,
]);
}
public function scanShareLookup()
@ -589,6 +628,7 @@ class AbdmGateway extends BaseController
'patient_id' => $patientId,
'abha_id' => $abhaId,
'consent_handle' => (string) ($consent['consent_handle'] ?? ''),
'consent_id' => $this->resolveConsentExternalId($consent),
'bundle_type' => (string) ($bundleRow['bundle_type'] ?? 'MedicationRequestBundle'),
'bundle' => $bundle,
];
@ -634,6 +674,7 @@ class AbdmGateway extends BaseController
'ok' => 1,
'queue_id' => $queueId,
'consent_handle' => (string) ($consent['consent_handle'] ?? ''),
'gateway_consent_id' => $this->resolveConsentExternalId($consent) !== '' ? $this->resolveConsentExternalId($consent) : null,
'status' => 'queued',
]);
}
@ -896,6 +937,22 @@ class AbdmGateway extends BaseController
'report_html' => trim((string) ($labReq->Report_Data ?? '')),
];
// ── Load LOINC code for the panel from lab_repo ───────────────────────
$labRepoRow = $this->db->table('lab_request lr')
->select('lr.lab_repo_id, repo.loinc_code AS repo_loinc_code, repo.Title')
->join('lab_repo repo', 'repo.mstRepoKey = lr.lab_repo_id', 'left')
->where('lr.id', $labReqId)
->get(1)
->getRowArray() ?? [];
$repoLoincCode = trim((string) ($labRepoRow['repo_loinc_code'] ?? ''));
if ($repoLoincCode !== '') {
$diagnosticReport['loinc_code'] = $repoLoincCode;
}
// ── Build structured observations from lab_request_item + lab_tests ───
$observations = $this->buildLabObservations($labReqId, (string) ($labReq->status ?? '0'));
$organization = $hospitalProfile['name'] !== ''
? ['name' => $hospitalProfile['name'], 'hfr_id' => $hospitalProfile['hfr_id']]
: null;
@ -907,7 +964,7 @@ class AbdmGateway extends BaseController
'period_start' => $reportedAt,
];
$bundle = $fhir->buildLabReportBundle($patient, $diagnosticReport, [], null, $organization, $encounter);
$bundle = $fhir->buildLabReportBundle($patient, $diagnosticReport, $observations, null, $organization, $encounter);
$bundleJson = (string) json_encode($bundle, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
// ── Store health_record ───────────────────────────────────────────────
@ -1990,6 +2047,142 @@ class AbdmGateway extends BaseController
};
}
/**
* Load lab_request_item rows for a lab request and build the structured
* observations array expected by FhirR4Builder::buildLabReportBundle().
*
* Each element contains:
* test_name, loinc_code, value_type (quantity|string), value,
* unit, ucum_code, ref_low, ref_high, interpretation, status
*
* @param int $labReqId lab_request.id
* @param string $status raw status from lab_request.status
* @return array<int, array<string, mixed>>
*/
private function buildLabObservations(int $labReqId, string $status): array
{
if (! $this->db->tableExists('lab_request_item') || ! $this->db->tableExists('lab_tests')) {
return [];
}
$rows = $this->db->table('lab_request_item lri')
->select(
'lri.lab_test_id, lri.lab_test_value, lri.lab_test_remark,' .
' lt.Test, lt.Unit, lt.FixedNormals, lt.FixedNormalsWomen,' .
' lt.loinc_code, lt.loinc_scale, lt.loinc_system, lt.loinc_property,' .
' lt.isGenderSpecific'
)
->join('lab_tests lt', 'lt.mstTestKey = lri.lab_test_id', 'left')
->where('lri.lab_request_id', $labReqId)
->orderBy('lri.id', 'ASC')
->get()
->getResultArray();
if (empty($rows)) {
return [];
}
$obsStatus = ($status == '1') ? 'final' : 'preliminary';
$observations = [];
foreach ($rows as $row) {
$rawValue = trim((string) ($row['lab_test_value'] ?? ''));
if ($rawValue === '' || strtolower($rawValue) === 'n/a') {
continue;
}
$testName = trim((string) ($row['Test'] ?? ''));
$loincCode = trim((string) ($row['loinc_code'] ?? ''));
$unit = trim((string) ($row['Unit'] ?? ''));
$scale = strtolower(trim((string) ($row['loinc_scale'] ?? '')));
// Determine value type: Quantitative = numeric value
$isNumeric = is_numeric($rawValue) && $scale !== 'nom' && $scale !== 'ord';
$valueType = $isNumeric ? 'quantity' : 'string';
// Parse reference range from "low-high" format (e.g. "12.00-16.00")
$refLow = $refHigh = '';
$normals = trim((string) ($row['FixedNormals'] ?? ''));
if ($normals !== '' && str_contains($normals, '-')) {
$parts = explode('-', $normals, 2);
if (count($parts) === 2 && is_numeric(trim($parts[0])) && is_numeric(trim($parts[1]))) {
$refLow = trim($parts[0]);
$refHigh = trim($parts[1]);
}
}
// Interpretation: H/L/N based on value vs reference range
$interpretation = 'N';
if ($isNumeric && $refLow !== '' && $refHigh !== '') {
$fVal = (float) $rawValue;
if ($fVal < (float) $refLow) {
$interpretation = 'L';
} elseif ($fVal > (float) $refHigh) {
$interpretation = 'H';
}
}
$obs = [
'test_name' => $testName !== '' ? $testName : ('Test-' . $row['lab_test_id']),
'loinc_code' => $loincCode,
'value_type' => $valueType,
'value' => $rawValue,
'unit' => $unit,
'ucum_code' => $this->unitToUcum($unit),
'ref_low' => $refLow,
'ref_high' => $refHigh,
'interpretation' => $interpretation,
'status' => $obsStatus,
];
if ($row['lab_test_remark'] !== '' && $row['lab_test_remark'] !== null) {
$obs['remark'] = trim((string) $row['lab_test_remark']);
}
$observations[] = $obs;
}
return $observations;
}
/**
* Best-effort map of common lab units to UCUM codes.
* Returns the original unit string when no UCUM equivalent is known.
*/
private function unitToUcum(string $unit): string
{
$unit = trim($unit);
if ($unit === '') {
return '';
}
$map = [
'g/dl' => 'g/dL',
'g/dL' => 'g/dL',
'mg/dl' => 'mg/dL',
'mg/dL' => 'mg/dL',
'mg/l' => 'mg/L',
'mmol/l' => 'mmol/L',
'umol/l' => 'umol/L',
'u/l' => 'U/L',
'iu/l' => 'IU/L',
'iu/ml' => 'IU/mL',
'cells/cumm'=> '10*3/uL',
'cells/mm3' => '10*3/uL',
'10^9/l' => '10*9/L',
'10^3/ul' => '10*3/uL',
'fl' => 'fL',
'pg' => 'pg',
'%' => '%',
'sec' => 's',
'min' => 'min',
'mmhg' => 'mm[Hg]',
'meq/l' => 'meq/L',
];
return $map[strtolower($unit)] ?? $unit;
}
/**
* Returns a lazily-constructed AbdmAuditService instance.
*/
@ -2024,21 +2217,52 @@ class AbdmGateway extends BaseController
}
$now = Time::now('Asia/Kolkata')->toDateTimeString();
$consentFields = $this->db->getFieldNames('abdm_consent_records') ?? [];
$builder = $this->db->table('abdm_consent_records')
->where('patient_id', $patientId)
->where('abha_id', $abhaId)
->where('status', 'GRANTED')
->where('expiry_date >=', $now);
->whereIn('consent_status', ['approved', 'granted'])
->groupStart()
->where('expires_at IS NULL', null, false)
->orWhere('expires_at >=', $now)
->groupEnd();
if ($consentHandle !== '') {
$builder->where('consent_handle', $consentHandle);
$builder->groupStart()->where('consent_handle', $consentHandle);
if (in_array('consent_id', $consentFields, true)) {
$builder->orWhere('consent_id', $consentHandle);
}
if (in_array('gateway_consent_id', $consentFields, true)) {
$builder->orWhere('gateway_consent_id', $consentHandle);
}
$builder->groupEnd();
}
$row = $builder->orderBy('id', 'DESC')->get(1)->getRowArray();
return ! empty($row) ? $row : null;
}
/**
* Pick the consent id expected by gateway: consent_id/gateway_consent_id fallback to consent_handle.
*
* @param array<string, mixed> $consent
*/
private function resolveConsentExternalId(array $consent): string
{
$consentId = trim((string) ($consent['consent_id'] ?? ''));
if ($consentId !== '') {
return $consentId;
}
$gatewayConsentId = trim((string) ($consent['gateway_consent_id'] ?? ''));
if ($gatewayConsentId !== '') {
return $gatewayConsentId;
}
return trim((string) ($consent['consent_handle'] ?? ''));
}
// =========================================================================
// M1 ABHA OTP Flows
// Calls EAtriaBridgeConnector synchronously — requires eatria_bridge connector.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,75 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
/**
* Adds consent id tracking columns to abdm_consent_records.
*
* gateway_consent_id captures consent id returned by the bridge/gateway so
* downstream share/audit flows can map consent_handle <-> gateway consent id.
*/
class AddConsentIdsToAbdmConsentRecords extends Migration
{
public function up(): void
{
if (! $this->db->tableExists('abdm_consent_records')) {
return;
}
$fields = $this->db->getFieldNames('abdm_consent_records') ?? [];
if (! in_array('consent_id', $fields, true)) {
$this->forge->addColumn('abdm_consent_records', [
'consent_id' => [
'type' => 'VARCHAR',
'constraint' => 120,
'null' => true,
'after' => 'consent_handle',
],
]);
}
$fields = $this->db->getFieldNames('abdm_consent_records') ?? [];
if (! in_array('gateway_consent_id', $fields, true)) {
$this->forge->addColumn('abdm_consent_records', [
'gateway_consent_id' => [
'type' => 'VARCHAR',
'constraint' => 120,
'null' => true,
'after' => 'consent_id',
],
]);
}
$indexes = $this->db->getIndexData('abdm_consent_records') ?? [];
$indexNames = array_map(static fn ($i) => (string) ($i->name ?? ''), $indexes);
if (! in_array('idx_abdm_consent_id', $indexNames, true)) {
$this->db->query('CREATE INDEX idx_abdm_consent_id ON abdm_consent_records (consent_id)');
}
if (! in_array('idx_abdm_gateway_consent_id', $indexNames, true)) {
$this->db->query('CREATE INDEX idx_abdm_gateway_consent_id ON abdm_consent_records (gateway_consent_id)');
}
}
public function down(): void
{
if (! $this->db->tableExists('abdm_consent_records')) {
return;
}
$fields = $this->db->getFieldNames('abdm_consent_records') ?? [];
if (in_array('gateway_consent_id', $fields, true)) {
$this->forge->dropColumn('abdm_consent_records', 'gateway_consent_id');
}
$fields = $this->db->getFieldNames('abdm_consent_records') ?? [];
if (in_array('consent_id', $fields, true)) {
$this->forge->dropColumn('abdm_consent_records', 'consent_id');
}
}
}

View File

@ -0,0 +1,160 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
/**
* Adds LOINC coding fields to pathology master tables so that
* lab test results can be published as structured FHIR Observations
* with standard LOINC codes for ABDM DiagnosticReportRecord.
*
* lab_tests → loinc_code, loinc_property, loinc_system, loinc_scale
* lab_repo → loinc_code (LOINC panel code for the DiagnosticReport.code)
*/
class AddLoincToPathologyTables extends Migration
{
public function up(): void
{
$this->addLabTestsLoincColumns();
$this->addLabRepoLoincColumn();
}
private function addLabTestsLoincColumns(): void
{
if (! $this->db->tableExists('lab_tests')) {
return;
}
$existing = $this->db->getFieldNames('lab_tests') ?? [];
$add = [];
if (! in_array('loinc_code', $existing, true)) {
$add['loinc_code'] = [
'type' => 'VARCHAR',
'constraint' => 20,
'null' => true,
'default' => null,
'after' => 'Unit',
];
}
if (! in_array('loinc_property', $existing, true)) {
$add['loinc_property'] = [
'type' => 'VARCHAR',
'constraint' => 50,
'null' => true,
'default' => null,
'after' => 'loinc_code',
];
}
if (! in_array('loinc_system', $existing, true)) {
$add['loinc_system'] = [
'type' => 'VARCHAR',
'constraint' => 50,
'null' => true,
'default' => null,
'after' => 'loinc_property',
];
}
if (! in_array('loinc_scale', $existing, true)) {
$add['loinc_scale'] = [
'type' => 'VARCHAR',
'constraint' => 20,
'null' => true,
'default' => null,
'after' => 'loinc_system',
];
}
if (! in_array('loinc_synced_at', $existing, true)) {
$add['loinc_synced_at'] = [
'type' => 'DATETIME',
'null' => true,
'default' => null,
'after' => 'loinc_scale',
];
}
if ($add !== []) {
$this->forge->addColumn('lab_tests', $add);
}
// Index for fast LOINC lookup
try {
$indexes = $this->db->query('SHOW INDEX FROM lab_tests')->getResultArray();
$hasIdx = false;
foreach ($indexes as $idx) {
if (strcasecmp((string) ($idx['Key_name'] ?? ''), 'idx_lab_tests_loinc') === 0) {
$hasIdx = true;
break;
}
}
if (! $hasIdx && in_array('loinc_code', $this->db->getFieldNames('lab_tests') ?? [], true)) {
$this->db->query('ALTER TABLE lab_tests ADD INDEX idx_lab_tests_loinc (loinc_code)');
}
} catch (\Throwable $e) {
// Non-blocking index creation
}
}
private function addLabRepoLoincColumn(): void
{
if (! $this->db->tableExists('lab_repo')) {
return;
}
$existing = $this->db->getFieldNames('lab_repo') ?? [];
if (! in_array('loinc_code', $existing, true)) {
$this->forge->addColumn('lab_repo', [
'loinc_code' => [
'type' => 'VARCHAR',
'constraint' => 20,
'null' => true,
'default' => null,
'after' => 'Title',
],
]);
}
if (! in_array('loinc_synced_at', $existing, true)) {
$this->forge->addColumn('lab_repo', [
'loinc_synced_at' => [
'type' => 'DATETIME',
'null' => true,
'default' => null,
'after' => 'loinc_code',
],
]);
}
}
public function down(): void
{
foreach (['loinc_code', 'loinc_property', 'loinc_system', 'loinc_scale', 'loinc_synced_at'] as $col) {
try {
if ($this->db->tableExists('lab_tests') &&
in_array($col, $this->db->getFieldNames('lab_tests') ?? [], true)) {
$this->forge->dropColumn('lab_tests', $col);
}
} catch (\Throwable $e) {
// no-op
}
}
foreach (['loinc_code', 'loinc_synced_at'] as $col) {
try {
if ($this->db->tableExists('lab_repo') &&
in_array($col, $this->db->getFieldNames('lab_repo') ?? [], true)) {
$this->forge->dropColumn('lab_repo', $col);
}
} catch (\Throwable $e) {
// no-op
}
}
try {
$this->db->query('ALTER TABLE lab_tests DROP INDEX idx_lab_tests_loinc');
} catch (\Throwable $e) {
// no-op
}
}
}

View File

@ -11,17 +11,18 @@ CREATE TABLE IF NOT EXISTS `lab_rgroups` (
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb3 ROW_FORMAT=DYNAMIC;
INSERT IGNORE INTO `lab_rgroups` (`mstRGrpKey`, `RepoGrp`, `PreFix`, `Suffix`, `LastNo`, `sort_order`, `Notes`) VALUES
('1', 'BIO CHEMISTRY', 'BIO', '', '0', '2', ''),
('2', 'Biopsy', 'BP', '', '0', '3', ''),
('3', 'CLINICAL PATHOLOGY', 'CP', '', '0', '4', ''),
('4', 'MICRO BIOLOGY', 'CUL', '', '0', '5', ''),
('5', 'General', 'GEN', '', '1', '6', ''),
('6', 'HAEMATOLOGY', 'HAE', '', '0', '1', ''),
('8', 'Urine', 'US', '', '0', '8', ''),
('9', 'Fluid Examination', 'FE', '0', '0', '9', '0'),
('10', 'CYTO-PATHOLOGY', 'CYP', '0', '0', '10', '0'),
('11', 'HISTOPATHOLOGY', 'HP', '0', '0', '11', '0'),
('12', 'IMMUNOLOGY-SEROLOGY', 'IS', '0', '0', '12', '0'),
('13', 'ENDOCRINOLOGY', 'ELO', '0', '0', '13', '0'),
('15', '', '0', '0', '0', '14', '0');
(1, 'Biochemistry', 'BCH', '', 0, 2, ''),
(2, 'Biopsy', 'BP', '', 0, 3, ''),
(3, 'Clinical Pathology', 'CP', '', 0, 4, ''),
(4, 'Infectious Disease', 'INF', '', 0, 5, ''),
(5, 'General', 'GEN', '', 1, 6, ''),
(6, 'Hematology', 'HEM', '', 0, 1, ''),
(8, 'Urine', 'US', '', 0, 8, ''),
(9, 'Fluid Examination', 'FE', '0', 0, 9, '0'),
(10, 'CYTO-PATHOLOGY', 'CYP', '0', 0, 10, '0'),
(11, 'HISTOPATHOLOGY', 'HP', '0', 0, 11, '0'),
(12, 'IMMUNOLOGY-SEROLOGY', 'IS', '0', 0, 12, '0'),
(13, 'Endocrinology', 'END', '0', 0, 13, '0'),
(16, 'Vitamins & Minerals', 'VTM', '', 0, 24, ''),
(18, 'Inflammatory Markers', 'INM', '', 0, 26, ''),
(19, 'Tumor Markers', 'TMR', '', 0, 27, '');

File diff suppressed because it is too large Load Diff

View File

@ -11,210 +11,15 @@ CREATE TABLE IF NOT EXISTS `lab_tests` (
`FixedNormals` varchar(30) NOT NULL DEFAULT '0',
`isGenderSpecific` int NOT NULL DEFAULT '0',
`FixedNormalsWomen` varchar(30) NOT NULL,
`loinc_code` varchar(20) DEFAULT NULL,
`loinc_property` varchar(50) DEFAULT NULL,
`loinc_system` varchar(50) DEFAULT NULL,
`loinc_scale` varchar(20) DEFAULT NULL,
`loinc_synced_at` datetime DEFAULT NULL,
PRIMARY KEY (`mstTestKey`),
UNIQUE KEY `TestID` (`TestID`),
KEY `Test` (`Test`)
KEY `Test` (`Test`),
KEY `idx_lab_tests_loinc` (`loinc_code`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb3;
INSERT IGNORE INTO `lab_tests` (`mstTestKey`, `Test`, `TestID`, `Result`, `Options`, `Formula`, `VRule`, `VMsg`, `Unit`, `FixedNormals`, `isGenderSpecific`, `FixedNormalsWomen`) VALUES
('1', 'HAEMOGLOBIN(Hb)', 'HB', '', NULL, '', '', '', 'gm%', '12.00-16.00', '0', ''),
('2', 'T.L.C.', 'TLC', '', NULL, '', '', '', 'per cu mm', '4000-11000', '0', ''),
('3', 'D.L.C.', 'DLC', '', NULL, '', '', '', '', '', '0', ''),
('4', 'NEUTROPHILS', 'NEU', '', NULL, '', '', '', '%', '45.00-70.00', '0', ''),
('5', 'LYMPHOCYTES', 'LYMP', '', NULL, '', '', '', '%', '20.00-45.00', '0', ''),
('6', 'Absolute EOSINOPHILS', 'AEOSIN', '', NULL, '', '', '', 'per cu mm', '40-440', '0', ''),
('7', 'MONOCYTES', 'MONO', '', NULL, '', '', '', '%', '1.00-6.00', '0', ''),
('8', 'BAND CELLS', 'BAND', '', NULL, '', '', '', '%', '1.00-6.00', '0', ''),
('9', 'IMMATURE CELLS', 'IMMATURE', '', NULL, '', '', '', '%', '', '0', ''),
('10', 'RBC', 'RBC', '', NULL, '', '', '', 'million/cm', '4.2-5.5', '0', ''),
('11', 'PCV', 'PCV', '', NULL, '', '', '', '%', '40.00-54.00', '0', ''),
('12', 'MCV', 'MCV', '', NULL, '', '', '', 'fl', '80.00-100.00', '0', ''),
('13', 'MCH', 'mch', '', NULL, '', '', '', 'pg', '25.00-35.00', '0', ''),
('14', 'MCHC', 'MCHC', '', NULL, '', '', '', 'g/dl', '28.00-38.00', '0', ''),
('15', 'PLATELET COUNT', 'PLATELET', '', NULL, '', '', '', 'Lakhs per ', '1.5-4.5', '0', ''),
('16', 'SERUM BILIRUBIN', 'SERBIL', '', NULL, '', '', '', 'mg%', '0.20-1.20', '0', ''),
('17', 'BLOOD UREA', 'BU', '', NULL, '', '', '', 'mg%', '11.00-37.00', '0', ''),
('18', 'SERUM CREATININE', 'SC', '', NULL, '', '', '', 'mg%', '0.60-1.40', '0', ''),
('19', 'SERUM URIC ACID', 'SUA', '', NULL, '', '', '', 'mg%', '2.6-7.2', '0', ''),
('20', 'SERUM SODIUM', 'SS', '', NULL, '', '', '', 'mmol/L', '135.00-155.00', '0', ''),
('21', 'SERUM POTASSIUM', 'SP', '', NULL, '', '', '', 'mmol/L', '3.50-5.50', '0', ''),
('22', 'SERUM CALCIUM', 'SCA', '', NULL, '', '', '', 'mg%', '8.60-10.3', '0', ''),
('23', 'TOTAL PROTEIN', 'TP', '', NULL, '', '', '', 'gm/dl', '6.4-8.3', '0', ''),
('24', 'SERUM ALBUMIN', 'SA', '', NULL, '', '', '', 'mg/dl', '3.50-5.2', '0', ''),
('25', 'SERUM GLOBULIN', 'SG', '', NULL, '', '', '', 'mg/dl', '2.4-4.0', '0', ''),
('26', 'ESR ', 'ESR1', '', NULL, '', '', '', 'MM', '0.00-9.00', '0', ''),
('27', 'SGOT/AST', 'SGOT', '', NULL, '', '', '', 'U/L', '5.00-35.00', '0', ''),
('28', 'SGPT/ALT', 'SGPT', '', NULL, '', '', '', 'U/L', '10.00-40.00', '0', ''),
('29', 'S. ALKALINE PHOSPHATASE', 'SALKALINE', '', NULL, '', '', '', 'U/L', '50.00-128.00', '0', ''),
('30', 'TOTAL PROTEIN', 'TOTPRO', '', NULL, '', '', '', 'mg%', '6.2-8.0', '0', ''),
('31', 'ALBUMIN', 'ALBU', '', NULL, '', '', '', 'mg%', '3.50-5.2', '0', ''),
('32', 'GLOBULIN', 'GLOB', '', NULL, '', '', '', 'mg%', '1.8-3.6', '0', ''),
('33', 'CRP Value', 'CRP', '', NULL, '', '', '', 'mg/l', '1-6', '0', ''),
('34', 'Body Part', 'BPART', '', NULL, '', '', '', '', '', '0', ''),
('35', 'HIV I', 'HIV1', '', NULL, '', '', '', '', 'NON-REACTIVE', '0', ''),
('36', 'HIV II', 'HIV2', '', NULL, '', '', '', '', 'NON-REACTIVE', '0', ''),
('37', 'Serum Prolectin', 'SProlectin', '', NULL, '', '', '', 'ng/ml', '5-35', '0', ''),
('38', 'Vitamin D', 'VITD', '', NULL, '', '', '', 'ng/ml', '20-100', '0', ''),
('39', 'HbA1c', 'HBA1C', '', NULL, '', '', '', '', '', '0', ''),
('40', 'Volume', 'vol', '', NULL, '', '', '', '', '', '0', ''),
('41', 'Colour', 'sem_color', 'Milky White', NULL, '', '', '', '', '', '0', ''),
('42', 'Viscosity', 'Viscosity', 'Viscous', NULL, '', '', '', '', '', '0', ''),
('43', 'Liquification Time', 'LIQTIME', '30', NULL, '', '', '', '', '0-30', '0', ''),
('44', 'Total Sperm Count', 'TOTSPCOUNT', '', NULL, '', '', '', 'millions p', '40-150', '0', ''),
('45', 'Motility', 'MOTILITY', '', NULL, '', '', '', '%', '', '0', ''),
('46', 'Non Motile', 'NONMOTILE', '', NULL, '', '', '', '', '0-20', '0', ''),
('47', 'Abnormal Forms', 'Abnormal', '', NULL, '', '', '', '', '0-30', '0', ''),
('48', 'Pus Cells', 'PUSCELL', '', NULL, '', '', '', 'per HPF', '1-2', '0', ''),
('49', 'Epithelial cells', 'EPITH', 'NIL', NULL, '', '', '', '', '', '0', ''),
('50', 'Test Head', 'THEAD', '', NULL, '', '', '', '', '', '0', ''),
('51', 'Test Head Value', 'THV', '', NULL, '', '', '', '', '', '0', ''),
('52', 'Glucose , Fasting (F)', 'BGR', '', NULL, '', '', '', 'mg%', '70.00-100.00', '0', ''),
('53', 'Patients Prothombin', 'PPTV', '', NULL, '', '', '', 'sec', '', '0', ''),
('54', 'Control Prothombin', 'CPTV', '18.0', NULL, '', '', '', '', '', '0', ''),
('55', 'Patient INR Value', 'PINRV', '', NULL, '', '', '', '', '', '0', ''),
('56', 'ORAL ANTICOAUGLANT THERAPEUTIC', 'OATR', '', NULL, '', '', '', '', '', '0', ''),
('57', 'Control APT', 'CAPT', '', NULL, '', '', '', '', '', '0', ''),
('58', 'Patient APT', 'PAPT', '', NULL, '', '', '', '', '', '0', ''),
('59', 'Blood Gluose Fasting', 'BGF', '', NULL, '', '', '', 'mg%', '60.00-110.00', '0', ''),
('61', 'Blood Glucose (PP)', 'BGPP', '', NULL, '', '', '', 'mg%', '80.00-140.00', '0', ''),
('62', 'Appearance', 'APP', '', NULL, '', '', '', '', '', '0', ''),
('63', 'Colour', 'COLOUR', '', NULL, '', '', '', '', '', '0', ''),
('64', 'Coagulum', 'COALGU', '', NULL, '', '', '', '', '', '0', ''),
('65', 'Proteins', 'PROTEINS', '', NULL, '', '', '', 'mg/dl', '', '0', ''),
('66', 'Sugar', 'SUGAR', '', NULL, '', '', '', 'mg/dl', '', '0', ''),
('67', 'Total Cell Count', 'TCC', '', NULL, '', '', '', 'per cmm', '', '0', ''),
('68', 'Polymorphs', 'Polymorphs', '', NULL, '', '', '', '', '', '0', ''),
('69', 'Lymphocytes', 'Lymphocyte', '', NULL, '', '', '', '', '', '0', ''),
('70', 'Monocytes', 'Monocytes', '', NULL, '', '', '', '', '', '0', ''),
('71', 'R.B.Cs', 'RBCS', '', NULL, '', '', '', '', '', '0', ''),
('72', 'Others', 'OTHER', '', NULL, '', '', '', '', '', '0', ''),
('73', 'Gram Stain', 'GRAMSTAIN', '', NULL, '', '', '', '', '', '0', ''),
('74', 'Stain For A.F.B.', 'AFB', '', NULL, '', '', '', '', '', '0', ''),
('75', 'Mesothelial', 'MESO', '', NULL, '', '', '', '', '', '0', ''),
('76', 'Triiodothyronine', 'T3', '', NULL, '', '', '', 'ng/ml', '0.60-1.51', '0', ''),
('77', 'Thyroxine', 'T4', '', NULL, '', '', '', 'ug/dl', '4.66-9.32', '0', ''),
('78', 'Thyroid Stimulating Hormone', 'TSH', '', NULL, '', '', '', 'ulU/mL', '0.25-5.00', '0', ''),
('79', 'Bilirubin Total', 'BT', '', NULL, '', '', '', '', '0.20-2.00', '0', ''),
('80', 'Bilirubin Direct', 'BD', '', NULL, '', '', '', '', '0-0.3', '0', ''),
('81', 'Bilirubin Indirect', 'BI', '', NULL, '', '', '', '', '0.00-0.70', '0', ''),
('82', 'Troponin', 'TROP', '', NULL, '', '', '', '', '', '0', ''),
('83', 'Troponine I (Trop I)', 'TROPI', '', NULL, '', '', '', '', '', '0', ''),
('84', 'SERUM PROTEIN', 'SPro', '', NULL, '', '', '', 'mg', '6.4-8.3', '0', ''),
('85', 'Colour', 'urine_colo', '', NULL, '', '', '', '', '', '0', ''),
('86', 'Transparency', 'urine_tran', '', NULL, '', '', '', '', '', '0', ''),
('87', 'PH', 'urine_ph', '', NULL, '', '', '', '', '4.7-7.5', '0', ''),
('88', 'Albumin', 'urine_albu', '', NULL, '', '', '', '', '', '0', ''),
('89', 'Sugar', 'urine_suga', '', NULL, '', '', '', '', '', '0', ''),
('90', 'Bile Salt', 'ub_salt', '', NULL, '', '', '', '', '', '0', ''),
('91', 'Bile Pigment', 'upigment', '', NULL, '', '', '', '', '', '0', ''),
('92', 'Pus Cells', 'u_puscell', '', NULL, '', '', '', '', '', '0', ''),
('93', 'Pus Cells', 'upuscells', '', NULL, '', '', '', '', '0-5', '0', ''),
('94', 'Epithial Cells', 'uepithial', '', NULL, '', '', '', '', '0-5', '0', ''),
('95', 'RBCs', 'urbc', '', NULL, '', '', '', '', '0-3', '0', ''),
('96', 'crystals', 'urinecryst', '', NULL, '', '', '', '', '', '0', ''),
('97', 'Specific Gravity', 'usg', 'QNS', NULL, '', '', '', '', '', '0', ''),
('98', 'Crystals', 'ucrystals', '', NULL, '', '', '', '', '', '0', ''),
('99', 'Cats', 'urinecasts', '', NULL, '', '', '', '', '', '0', ''),
('100', 'Cholesterol', 'l_Choleste', '', NULL, '', '', '', '', '130-200', '0', ''),
('101', 'Triglyceride', 'l_Triglyce', '', NULL, '', '', '', '', '45-160', '0', '');
INSERT IGNORE INTO `lab_tests` (`mstTestKey`, `Test`, `TestID`, `Result`, `Options`, `Formula`, `VRule`, `VMsg`, `Unit`, `FixedNormals`, `isGenderSpecific`, `FixedNormalsWomen`) VALUES
('102', 'HDL', 'l_hdl', '', NULL, '', '', '', '', '35-79', '0', ''),
('103', 'LDL', 'l_ldl', '', NULL, '', '', '', '', '60-140', '0', ''),
('104', 'VLDL', 'l_vldl', '', NULL, '', '', '', '', '10-35', '0', ''),
('105', 'Bleeding Time', 'blood_bt', '', NULL, '', '', '', 'min', '0-7', '0', ''),
('106', 'Clotting Time', 'blood_ct', '', NULL, '', '', '', 'min', '4.00-12.00', '0', ''),
('107', 'HCV', 'hcv', '', NULL, '', '', '', '', '', '0', ''),
('108', 'HBsAG', 'HBsAG', '', NULL, '', '', '', '', '', '0', ''),
('109', 'Serum Amylase', 'AMYLASE', '', NULL, '', '', '', '', '20-80', '0', ''),
('110', 'Serum Lipase', 'LIPASE', '', NULL, '', '', '', '', '0-60', '0', ''),
('111', 'R A Factor', 'RAFACTOR', '', NULL, '', '', '', '', '0-25', '0', ''),
('112', 'TLC Value', 'TLC1', '', NULL, '', '', '', '', '4000-11000', '0', ''),
('113', 'Serum Cholesterol', 'Cholestero', '', NULL, '', '', '', '', '130-200', '0', ''),
('114', 'Serum Trigycerides', 'Trigycerid', '', NULL, '', '', '', '', '45-160', '0', ''),
('115', 'ABO', 'ABO', '', NULL, '', '', '', '', '', '0', ''),
('116', 'Rh Factor', 'RHF', '', NULL, '', '', '', '', '', '0', ''),
('117', 'PREGNANCY Test', 'PREGNANCYT', '', NULL, '', '', '', '', '', '0', ''),
('118', 'TYPHOID IgM', 'lgm', '', NULL, '', '', '', '', '', '0', ''),
('119', 'MALARIA ANTIGEN', 'MALARIA', '', NULL, '', '', '', '', '', '0', ''),
('120', 'TYPHOID IgG', 'IgG', '', NULL, '', '', '', '', '', '0', ''),
('121', 'TYPHOID IgM', 'IgM', '', NULL, '', '', '', '', '', '0', ''),
('122', 'GAMMA GT', 'GAMAGT', '', NULL, '', '', '', '', '09-45', '0', ''),
('123', 'CPK MB', 'CPKMB', '', NULL, '', '', '', '', '0-24', '0', ''),
('124', 'CPK-NAC', 'CPNACK', '', NULL, '', '', '', '', '35-174', '0', ''),
('125', 'PSA(PROSTATIC SPE. ANTIGEN)', 'PSA', '', NULL, '', '', '', '', '', '0', ''),
('126', 'ASO', 'ASO', '', NULL, '', '', '', '', '0-200', '0', ''),
('127', 'Dengue Ns1', 'DNS1', '', NULL, '', '', '', '', '', '0', ''),
('128', 'Dengue IgM', 'DIGM', '', NULL, '', '', '', '', '', '0', ''),
('129', 'Dengue IgG ', 'DIGG', '', NULL, '', '', '', '', '', '0', ''),
('130', 'DCT', 'DCT', '', NULL, '', '', '', '', '', '0', ''),
('131', 'Stool for Occult Blood', 'occult', 'Stool for Occult Blood', NULL, '', '', '', '', '', '0', ''),
('132', 'RETICULOCYTE COUNT', 'Reticount', 'RETICULOCYTE COUNT', NULL, '', '', '', '%', '0.20-2.00', '0', ''),
('133', 'BILE SALT', 'BS', '', NULL, '', '', '', '', '', '0', ''),
('134', 'BILE PIGMENT', 'BP', '', NULL, '', '', '', '', '', '0', ''),
('135', 'V.D.R.L', 'V.D.R.L', '', NULL, '', '', '', '', '', '0', ''),
('136', 'G6PD', '{g6pd}', 'G6PD', NULL, '', '', '', '', '', '0', ''),
('137', 'G6PD', 'G6PD', '', NULL, '', '', '', '', '6.40-18.7', '0', ''),
('138', 'Indian Link', 'India Link', '', NULL, '', '', '', '', '', '0', ''),
('139', 'LDH', 'LDH', '', NULL, '', '', '', '', '114-240', '0', ''),
('140', 'SAFB', 'SAFB', '', NULL, '', '', '', '', '', '0', ''),
('141', 'MALIGNANT CELLS', 'MC', '', NULL, '', '', '', '', '', '0', ''),
('142', 'Color', 'stool_colo', '', NULL, '', '', '', '', '', '0', ''),
('143', 'Consistency', 'Consistenc', '', NULL, '', '', '', '', '', '0', ''),
('144', 'Mucus', 'Mucus', '', NULL, '', '', '', '', '', '0', ''),
('145', 'PH', 'SPH', '', NULL, '', '', '', '', '', '0', ''),
('146', 'Reducing Sugar', 'RSugar', '', NULL, '', '', '', '', '', '0', ''),
('147', 'Pus Cells', 'PCells', '', NULL, '', '', '', '', '', '0', ''),
('148', 'RBCs', 'sRBC', 'NIL', NULL, '', '', '', '', '', '0', ''),
('149', 'Ova', 'Ova', '', NULL, '', '', '', '', '', '0', ''),
('150', 'Cyst', 'Cyst', '', NULL, '', '', '', '', '', '0', ''),
('151', 'Fat Globules', 'FatGlobuli', '', NULL, '', '', '', '', '', '0', ''),
('152', 'S Typhi O 1:20', 'STyphiO1', '', NULL, '', '', '', '', '', '0', ''),
('153', 'S Typhi O 1 :40', 'STyphiO2', '', NULL, '', '', '', '', '', '0', ''),
('154', 'S Typhi O 1:80', 'STyphiO3', '', NULL, '', '', '', '', '', '0', ''),
('155', 'S Typhi O 1:160', 'STyphiO4', '', NULL, '', '', '', '', '', '0', ''),
('156', 'S Typhi O 1:320', 'STyphiO5', '', NULL, '', '', '', '', '', '0', ''),
('157', 'S Typhi H 1:20', 'STyphiH1', '', NULL, '', '', '', '', '', '0', ''),
('158', 'S Typhi H 1:40', 'STyphiH2', '', NULL, '', '', '', '', '', '0', ''),
('159', 'S Typhi H 1:80', 'STyphiH3', '', NULL, '', '', '', '', '', '0', ''),
('160', 'S Typhi H 1:320', 'STyphiH5', '', NULL, '', '', '', '', '', '0', ''),
('161', 'S Para Typhi AH 1:20', 'SParaTyphiAH1', '', NULL, '', '', '', '', '', '0', ''),
('162', 'S Para Typhi AH 1:40', 'SParaTyphiAH2', '', NULL, '', '', '', '', '', '0', ''),
('163', 'S Para Typhi AH 1:80', 'SParaTyphiAH3', '', NULL, '', '', '', '', '', '0', ''),
('164', 'S Para Typhi AH 1:160', 'SParaTyphiAH4', '', NULL, '', '', '', '', '', '0', ''),
('165', 'S Para Typhi AH 1:320', 'SParaTyphiAH5', '', NULL, '', '', '', '', '', '0', ''),
('166', 'S Para Typhi BH 1:20', 'SParaTyphiBH1', '', NULL, '', '', '', '', '', '0', ''),
('167', 'S Para Typhi BH 1:40', 'SParaTyphiBH2', '', NULL, '', '', '', '', '', '0', ''),
('168', 'S Para Typhi BH 1:80', 'SParaTyphiBH3', '', NULL, '', '', '', '', '', '0', ''),
('169', 'S Para Typhi BH 1:160', 'SParaTyphiBH4', '', NULL, '', '', '', '', '', '0', ''),
('170', 'S Para Typhi BH 1:320', 'SParaTyphiBH5', '', NULL, '', '', '', '', '', '0', ''),
('171', 'Indirect Coombs Test', 'ICT', '', NULL, '', '', '', '', '', '0', ''),
('172', 'Urine for Ketone', 'UFK', '', NULL, '', '', '', '', '', '0', ''),
('173', 'MP', 'MALARIAP', '', NULL, '', '', '', '', '', '0', ''),
('174', 'Blood Urea Nitrogen', 'BUN', '', NULL, '', '', '', '', '6-24', '0', ''),
('175', 'Banejone Protein', 'BanP', '', NULL, '', '', '', '', '', '0', ''),
('176', 'Urine For Protien', 'UFP', '', NULL, '', '', '', '', '', '0', ''),
('177', 'Chikungunya IgM', 'CIGM', '', NULL, '', '', '', '', '', '0', ''),
('178', 'P.P.D Given', 'PPDG', '', NULL, '', '', '', '', '', '0', ''),
('179', 'Montoux Reading On', 'READON', '', NULL, '', '', '', '', '', '0', ''),
('180', 'Montoux Induration', 'INDURATION', '', NULL, '', '', '', '', '', '0', ''),
('181', 'Montoux Result', 'MONTEST', '', NULL, '', '', '', '', '', '0', ''),
('182', 'EOSINOPHILS', 'EOSIN', '', NULL, '', '', '', '', '1-8', '0', ''),
('183', 'SERUM PHOSPHORUS', 'SPHOSPHORUS', '', NULL, '', '', '', 'mg/dl', '2.5-5.0', '0', ''),
('184', 'Ionised Calcium', 'ICALCIUM', '', NULL, '', '', '', 'mg/dl', '4.4-5.5', '0', ''),
('185', 'Prothrombin Time ', 'PT', '', NULL, '', '', '', 'secs', '11-16', '0', ''),
('186', 'Glucose Random', 'GR', '', NULL, '', '', '', 'mg/dl', '70-140', '0', ''),
('187', 'A:G Ratio', 'AGR', '', NULL, '', '', '', '', '1.1-2.0', '0', ''),
('188', 'BASOPHILES', 'BASO', '', NULL, '', '', '', '%', '0-1', '0', ''),
('190', 'Bacteria flora few ', 'BFF', '', NULL, '', '', '', '', '', '0', ''),
('191', 'Semen Reaction', 'semen_reaction', '', NULL, '', '', '', '', 'ALKALINE', '0', ''),
('192', 'SEMENREMARKS', 'SEMENREMARKS', '', NULL, '', '', '', '', '', '0', ''),
('193', 'Semen Active', 'semen_active', '', NULL, '', '', '', '', '0-60', '0', ''),
('194', 'Sluggish', 'SLUGGISH', '', NULL, '', '', '', '%', '0-30', '0', ''),
('195', 'CRP Status', 'CRPSTATUS', '', NULL, '', '', '', '', '', '0', ''),
('196', 'VITAMIN12', 'VITAMIN12', '', NULL, '', '', '', 'ng/ml', '211-911', '0', ''),
('197', 'DIMER', 'DIMER', '0', NULL, '', '', '', 'ug/ml', '0.0-0.5', '0', ''),
('198', 'Beta HCG', 'BHCG', '', NULL, '', '', '', 'mIU/mL', '0-5.3', '0', ''),
('199', 'Covid-19 Antgen Test', 'covid_antigen_t', '', NULL, '', '', '', '', '', '0', ''),
('200', 'Glucose, Post Prandial (PP)', 'AGR1', '', NULL, '', '', '', '', '70.00-140.00', '0', '');
-- No rows found in `lab_tests`

View File

@ -11,196 +11,4 @@ CREATE TABLE IF NOT EXISTS `lab_tests_option` (
KEY `option_value` (`option_value`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
INSERT IGNORE INTO `lab_tests_option` (`id`, `mstTestKey`, `sort_id`, `option_value`, `option_text`, `option_bold`) VALUES
('74', '35', '1', 'NON-REACTIVE', 'NON-REACTIVE', '0'),
('75', '35', '2', 'REACTIVE', 'REACTIVE', '0'),
('76', '36', '1', 'NON-REACTIVE', 'NON-REACTIVE', '0'),
('77', '36', '2', 'REACTIVE', 'REACTIVE', '0'),
('22', '41', '1', 'Milky_White', 'Milky_White', '0'),
('206', '41', '4', 'brownish or red', 'brownish or red', '0'),
('208', '41', '5', 'Reddish in color ', 'Reddish in color ', '0'),
('209', '41', '6', 'grey in color ', 'grey in color ', '0'),
('23', '42', '1', 'Viscous', 'Viscous', '0'),
('210', '42', '2', 'Thin ', 'Thin ', '0'),
('211', '42', '3', 'Thick', 'Thick', '0'),
('24', '50', '1', 'Serum', 'Serum', '0'),
('25', '50', '2', 'Plasma', 'Plasma', '0'),
('26', '50', '3', 'Pleural', 'Pleural', '0'),
('27', '50', '4', 'Pericadial & Ascitic Fluids', 'Pericadial', '0'),
('28', '62', '1', 'CLEAR', 'CLEAR', '0'),
('219', '62', '3', 'Semi_Turbid', 'Semi_Turbid', '0'),
('201', '62', '4', 'Turbid', 'Turbid', '0'),
('29', '64', '1', 'ABSENT', 'ABSENT', '0'),
('30', '64', '2', 'PRESENT', 'PRESENT', '0'),
('31', '73', '1', 'No_Organism_Seen', 'No_Organism_Seen', '0'),
('32', '73', '2', 'Positive', 'Positive', '0'),
('33', '74', '1', 'Not-Detected', 'Not-Detected', '0'),
('34', '74', '2', 'Postive_1', '+1', '0'),
('35', '74', '3', 'Postive_2', '+2', '0'),
('36', '74', '4', 'Postive_3', '+3', '0'),
('37', '74', '5', 'Postive_4', '+4', '0'),
('38', '82', '1', 'Positive', 'Positive', '0'),
('39', '82', '2', 'Negative', 'Negative', '0'),
('40', '83', '1', 'Positive', 'Positive', '0'),
('41', '83', '2', 'Negative', 'Negative', '0'),
('42', '85', '1', 'Yellow', 'Yellow', '0'),
('43', '85', '2', 'Pale-Yellow', 'Pale-Yellow', '0'),
('44', '85', '3', 'Reddish_Deep_Yellow', 'Reddish_Deep_Yellow', '0'),
('93', '85', '4', 'Reddish', 'Reddish', '0'),
('94', '85', '5', 'Deep-Yellow', 'Deep-Yellow', '0'),
('45', '86', '1', 'Clear', 'Clear', '0'),
('46', '86', '2', 'Turbid', 'Turbid', '0'),
('49', '88', '1', 'NIL', 'NIL', '0'),
('88', '88', '2', 'Trace', 'Trace', '0'),
('50', '88', '3', 'Present +', 'Present+', '0'),
('51', '88', '4', 'Present++', 'Present++', '0'),
('52', '88', '5', 'Present+++', 'Present+++', '0'),
('53', '88', '6', 'Present++++', 'Present++++', '0'),
('54', '89', '1', 'Nil', 'Nil', '0'),
('99', '89', '2', 'Trace', 'Trace', '0'),
('55', '89', '3', 'Present+', 'Present+', '0'),
('56', '89', '4', 'Present++', 'Present++', '0'),
('57', '89', '5', 'Present+++', 'Present+++', '0'),
('58', '89', '6', 'Present++++', 'Present++++', '0'),
('59', '90', '1', 'Nil', 'Nil', '0'),
('60', '90', '2', 'Present', 'Present', '0'),
('61', '91', '1', 'Nil', 'Nil', '0'),
('62', '91', '2', 'Present', 'Present', '0'),
('63', '92', '1', '0-1', '0-1', '0'),
('64', '92', '2', '1-2', '1-2', '0'),
('65', '96', '1', 'Nil', 'NIL', '0'),
('66', '96', '2', 'Calcium-Oxalate', 'Calcium-Oxalate', '0'),
('67', '96', '3', 'Phosphate', 'Phosphate', '0'),
('68', '96', '4', 'UricAcid', 'UricAcid', '0'),
('116', '97', '1', 'Qns', 'Qns', '0'),
('69', '99', '1', 'Nil', 'Nil', '0'),
('70', '99', '2', 'Present Hyaline', 'Present Hyaline', '0'),
('71', '99', '3', 'Granular', 'Granular', '0'),
('72', '107', '1', 'Non-Reactive', 'Non-Reactive', '0'),
('73', '107', '2', 'Reactive', 'Reactive', '0'),
('78', '108', '1', 'NON-REACTIVE', 'NON-REACTIVE', '0'),
('79', '108', '2', 'REACTIVE', 'REACTIVE', '0'),
('80', '115', '1', 'A', 'A', '0'),
('81', '115', '2', 'B', 'B', '0'),
('82', '115', '3', 'AB', 'AB', '0'),
('83', '115', '4', 'O', 'O', '0'),
('84', '116', '1', 'POSITIVE', 'POSITIVE', '0'),
('85', '116', '2', 'NEGATIVE', 'NEGATIVE', '0'),
('86', '117', '1', 'Positive', 'Positive', '0'),
('87', '117', '2', 'Negative', 'Negative', '0'),
('89', '118', '1', 'Non-Reactive', 'Non-Reactive', '0'),
('90', '118', '2', 'Reactive', 'Reactive', '0'),
('91', '119', '1', 'NON-REACTIVE', 'NON-REACTIVE', '0'),
('92', '119', '2', 'REACTIVE', 'REACTIVE', '0'),
('95', '120', '1', 'Negative', 'Negative', '0'),
('96', '120', '2', 'Positive', 'Positive', '0'),
('97', '121', '1', 'Negative', 'Negative', '0'),
('98', '121', '2', 'Positive', 'Positive', '0'),
('101', '127', '1', 'NON-REACTIVE', 'NON-REACTIVE', '0'),
('100', '127', '2', 'REACTIVE', 'REACTIVE', '0'),
('103', '128', '1', 'NON-REACTIVE', 'NON-REACTIVE', '0'),
('102', '128', '2', 'REACTIVE', 'REACTIVE', '0'),
('105', '129', '1', 'NON-REACTIVE', 'NON-REACTIVE', '0'),
('104', '129', '2', 'REACTIVE', 'REACTIVE', '0'),
('106', '130', '1', 'NEGATIVE', 'NEGATIVE', '0'),
('107', '130', '2', 'POSITIVE', 'POSITIVE', '0'),
('108', '131', '1', 'Negative', 'Negative', '0'),
('109', '131', '2', 'Positive', 'Positive', '0'),
('110', '133', '1', 'NEGATIVE', 'NEGATIVE', '0'),
('111', '133', '2', 'POSITIVE', 'POSITIVE', '0'),
('112', '134', '1', 'NEGATIVE', 'NEGATIVE', '0'),
('113', '134', '2', 'POSITIVE', 'POSITIVE', '0'),
('178', '135', '3', 'Non-Reactive', 'Non-Reactive', '0'),
('179', '135', '4', 'Reactive', 'Reactive', '0');
INSERT IGNORE INTO `lab_tests_option` (`id`, `mstTestKey`, `sort_id`, `option_value`, `option_text`, `option_bold`) VALUES
('117', '138', '1', 'Negative', 'Negative', '0'),
('118', '138', '2', 'Positive', 'Positive', '0'),
('119', '140', '1', 'Negative', 'Negative', '0'),
('120', '140', '2', 'Postive+', 'Postive', '0'),
('121', '140', '3', 'Postive++', 'Postive++', '0'),
('122', '140', '4', 'Postive+++', 'Postive+++', '0'),
('123', '140', '5', 'Postive++++', 'Postive++++', '0'),
('124', '141', '1', 'Seen', 'Seen', '0'),
('125', '141', '2', 'Not Seen', 'Not Seen', '0'),
('126', '142', '1', 'Brownish', 'Brownish', '0'),
('127', '142', '2', 'Yellow', 'Yellow', '0'),
('128', '142', '3', 'Greenish', 'Greenish', '0'),
('129', '143', '1', 'Loose', 'Loose', '0'),
('130', '143', '2', 'Semi Formed', 'Semi Formed', '0'),
('131', '143', '3', 'Fully Formed', 'Fully Formed', '0'),
('132', '144', '1', 'Absent', 'Absent', '0'),
('133', '144', '2', 'Present', 'Present', '0'),
('134', '146', '1', 'Postive', 'Postive', '0'),
('135', '146', '2', 'Negative', 'Negative', '0'),
('136', '151', '1', 'Present', 'Present', '0'),
('137', '151', '2', 'NIL', 'NIL', '0'),
('139', '152', '1', '-', '-', '0'),
('138', '152', '2', '+', '+', '0'),
('140', '153', '1', '-', '-', '0'),
('141', '153', '2', '+', '+', '0'),
('142', '154', '1', '-', '-', '0'),
('143', '154', '2', '+', '+', '0'),
('144', '155', '1', '-', '-', '0'),
('145', '155', '2', '+', '+', '0'),
('146', '156', '1', '-', '-', '0'),
('147', '156', '2', '+', '+', '0'),
('148', '157', '1', '-', '-', '0'),
('149', '157', '2', '+', '+', '0'),
('150', '158', '1', '-', '-', '0'),
('151', '158', '2', '+', '+', '0'),
('152', '159', '1', '-', '-', '0'),
('153', '159', '2', '+', '+', '0'),
('154', '160', '1', '-', '-', '0'),
('155', '160', '2', '+', '+', '0'),
('156', '161', '1', '-', '-', '0'),
('157', '161', '2', '+', '+', '0'),
('158', '162', '1', '-', '-', '0'),
('159', '162', '2', '+', '+', '0'),
('160', '163', '1', '-', '-', '0'),
('161', '163', '2', '+', '+', '0'),
('162', '164', '1', '-', '-', '0'),
('163', '164', '2', '+', '+', '0'),
('164', '165', '1', '-', '-', '0'),
('165', '165', '2', '+', '+', '0'),
('166', '166', '1', '-', '-', '0'),
('167', '166', '2', '+', '+', '0'),
('168', '167', '1', '-', '-', '0'),
('169', '167', '2', '+', '+', '0'),
('170', '168', '1', '-', '-', '0'),
('171', '168', '2', '+', '+', '0'),
('172', '169', '1', '-', '-', '0'),
('173', '169', '2', '+', '+', '0'),
('174', '170', '1', '-', '-', '0'),
('175', '170', '2', '+', '+', '0'),
('176', '171', '1', 'Negative', 'Negative', '0'),
('177', '171', '2', 'Positive', 'Positive', '0'),
('180', '172', '1', 'Absent', 'Absent', '0'),
('181', '172', '2', 'Present +', 'Present +', '0'),
('182', '172', '3', 'Present ++', 'Present ++', '0'),
('183', '172', '4', 'Present +++', 'Present +++', '0'),
('184', '172', '5', 'Present ++++', 'Present ++++', '0'),
('185', '173', '1', 'Not-Seen', 'Not-Seen', '0'),
('186', '173', '2', 'Seen', 'Seen', '1'),
('187', '175', '1', 'Negative', 'Negative', '0'),
('188', '175', '2', 'Positive', 'Positive', '1'),
('189', '176', '1', 'Absent', 'Absent', '0'),
('190', '176', '2', 'Present', 'Present', '1'),
('191', '177', '1', 'Negative', 'Negative', '0'),
('192', '177', '2', 'Positive', 'Positive', '1'),
('193', '181', '1', 'Negative', 'Negative', '0'),
('194', '181', '2', 'Positive', 'Positive', '0'),
('199', '190', '1', 'Nil', 'Nil', '0'),
('195', '190', '2', '+', '+', '0'),
('196', '190', '3', '+ +', '+ +', '0'),
('197', '190', '4', '+ + +', '+ + +', '0'),
('198', '190', '5', '+ + + +', '+ + + +', '0'),
('202', '191', '1', 'ALKALINE', 'ALKALINE', '0'),
('212', '191', '3', 'Acidic', 'Acidic', '0'),
('213', '192', '1', 'Normal', 'Normal', '0'),
('214', '192', '2', ' Abnormal', ' Abnormal', '0'),
('217', '195', '1', 'Negative', 'Negative', '0'),
('218', '195', '2', 'Positive', 'Positive', '1'),
('220', '199', '1', 'Negative', 'Negative', '0'),
('221', '199', '2', 'Positive', 'Positive', '1');
-- No rows found in `lab_tests_option`

View File

@ -8,336 +8,4 @@ CREATE TABLE IF NOT EXISTS `lab_repotests` (
UNIQUE KEY `mstRepoKey_EOrder` (`mstRepoKey`,`EOrder`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb3;
INSERT IGNORE INTO `lab_repotests` (`id`, `mstRepoKey`, `mstTestKey`, `EOrder`) VALUES
('1', '1', '1', '1'),
('2', '1', '2', '2'),
('4', '1', '4', '4'),
('5', '1', '5', '5'),
('270', '1', '182', '7'),
('7', '1', '7', '8'),
('276', '1', '188', '11'),
('10', '1', '10', '12'),
('15', '1', '15', '13'),
('12', '1', '12', '14'),
('13', '1', '13', '15'),
('14', '1', '14', '16'),
('11', '1', '11', '17'),
('278', '1', '26', '18'),
('16', '2', '16', '1'),
('180', '2', '80', '2'),
('181', '2', '81', '3'),
('27', '2', '27', '4'),
('28', '2', '28', '5'),
('29', '2', '29', '6'),
('30', '2', '30', '7'),
('31', '2', '31', '8'),
('32', '2', '32', '9'),
('275', '2', '187', '10'),
('17', '3', '17', '1'),
('18', '3', '18', '2'),
('19', '3', '19', '3'),
('22', '3', '22', '4'),
('20', '3', '20', '5'),
('21', '3', '21', '6'),
('33', '4', '33', '1'),
('286', '4', '195', '2'),
('34', '5', '34', '1'),
('35', '6', '35', '1'),
('36', '6', '36', '2'),
('160', '7', '108', '1'),
('37', '8', '37', '1'),
('38', '9', '38', '1'),
('39', '10', '39', '1'),
('40', '11', '40', '1'),
('41', '11', '41', '2'),
('280', '11', '191', '3'),
('42', '11', '42', '4'),
('43', '11', '43', '5'),
('44', '11', '44', '6'),
('282', '11', '193', '7'),
('283', '11', '194', '9'),
('46', '11', '46', '10'),
('47', '11', '47', '11'),
('48', '11', '48', '12'),
('49', '11', '49', '13'),
('285', '11', '148', '14'),
('281', '11', '192', '15'),
('50', '12', '50', '1'),
('51', '12', '51', '2'),
('52', '13', '52', '1'),
('451', '13', '200', '2'),
('53', '14', '53', '1'),
('54', '14', '54', '2'),
('55', '14', '55', '3'),
('56', '14', '56', '4'),
('273', '15', '185', '1'),
('58', '15', '58', '2'),
('57', '15', '57', '3'),
('59', '16', '59', '1'),
('60', '16', '60', '2'),
('61', '17', '61', '1'),
('62', '18', '62', '1'),
('63', '18', '63', '2'),
('64', '18', '64', '3'),
('65', '18', '65', '4'),
('66', '18', '66', '5'),
('67', '18', '67', '6'),
('68', '18', '68', '7'),
('69', '18', '69', '8'),
('70', '18', '70', '9'),
('71', '18', '71', '10'),
('72', '18', '72', '11'),
('73', '18', '73', '12'),
('74', '18', '74', '13'),
('89', '19', '76', '1'),
('90', '19', '77', '2'),
('91', '19', '78', '3'),
('75', '20', '62', '1'),
('76', '20', '63', '2'),
('77', '20', '64', '3'),
('78', '20', '65', '4'),
('79', '20', '66', '5'),
('80', '20', '67', '6'),
('81', '20', '68', '7'),
('82', '20', '69', '8'),
('88', '20', '75', '10'),
('84', '20', '71', '12'),
('86', '20', '73', '13'),
('87', '20', '74', '14'),
('92', '21', '79', '1'),
('93', '21', '80', '2'),
('94', '21', '81', '3'),
('95', '22', '82', '1'),
('96', '23', '83', '1');
INSERT IGNORE INTO `lab_repotests` (`id`, `mstRepoKey`, `mstTestKey`, `EOrder`) VALUES
('97', '24', '84', '1'),
('98', '25', '16', '1'),
('99', '26', '27', '1'),
('100', '27', '28', '1'),
('165', '28', '112', '1'),
('102', '29', '4', '2'),
('103', '29', '5', '3'),
('269', '29', '182', '5'),
('105', '29', '7', '6'),
('106', '29', '8', '7'),
('107', '29', '9', '8'),
('113', '30', '4', '1'),
('114', '31', '5', '1'),
('116', '33', '10', '1'),
('117', '34', '11', '1'),
('118', '35', '12', '1'),
('119', '36', '13', '1'),
('120', '37', '14', '1'),
('121', '38', '15', '1'),
('122', '39', '17', '1'),
('123', '40', '18', '1'),
('124', '41', '19', '1'),
('125', '42', '20', '1'),
('126', '43', '21', '1'),
('127', '44', '22', '1'),
('128', '45', '24', '1'),
('129', '46', '25', '1'),
('130', '47', '26', '1'),
('131', '48', '29', '1'),
('133', '50', '73', '1'),
('134', '51', '76', '1'),
('135', '52', '77', '1'),
('136', '54', '1', '1'),
('137', '55', '85', '1'),
('138', '55', '86', '2'),
('149', '55', '97', '3'),
('139', '55', '87', '4'),
('140', '55', '88', '5'),
('141', '55', '89', '6'),
('145', '55', '93', '9'),
('146', '55', '94', '10'),
('147', '55', '95', '11'),
('151', '55', '99', '12'),
('148', '55', '96', '13'),
('279', '55', '190', '14'),
('152', '56', '100', '1'),
('154', '56', '102', '2'),
('155', '56', '103', '3'),
('156', '56', '104', '4'),
('153', '56', '101', '5'),
('157', '57', '105', '1'),
('158', '57', '106', '2'),
('159', '58', '107', '1'),
('161', '59', '78', '1'),
('164', '62', '111', '1'),
('234', '63', '152', '1'),
('235', '63', '153', '2'),
('236', '63', '154', '3'),
('237', '63', '155', '4'),
('238', '63', '156', '5'),
('239', '63', '157', '6'),
('240', '63', '158', '7'),
('241', '63', '159', '8'),
('242', '63', '160', '9'),
('243', '63', '161', '10'),
('244', '63', '162', '11'),
('245', '63', '163', '12'),
('246', '63', '164', '13'),
('247', '63', '165', '14'),
('248', '63', '166', '15'),
('249', '63', '167', '16'),
('250', '63', '168', '17'),
('251', '63', '169', '18'),
('252', '63', '170', '19'),
('162', '64', '109', '1'),
('163', '65', '110', '1'),
('166', '66', '113', '1'),
('167', '67', '114', '1'),
('168', '68', '115', '1'),
('169', '68', '116', '2'),
('170', '69', '117', '1'),
('224', '70', '142', '1'),
('225', '70', '143', '2'),
('226', '70', '144', '3'),
('227', '70', '145', '4'),
('228', '70', '146', '5'),
('229', '70', '147', '6'),
('230', '70', '148', '7'),
('231', '70', '149', '8'),
('232', '70', '150', '9'),
('233', '70', '151', '10'),
('174', '71', '121', '2'),
('173', '71', '120', '3'),
('172', '72', '119', '1'),
('175', '73', '122', '1'),
('177', '74', '124', '1'),
('176', '75', '123', '1'),
('178', '76', '125', '1'),
('187', '77', '132', '1'),
('179', '78', '126', '1');
INSERT IGNORE INTO `lab_repotests` (`id`, `mstRepoKey`, `mstTestKey`, `EOrder`) VALUES
('182', '79', '127', '1'),
('183', '79', '128', '2'),
('184', '79', '129', '3'),
('185', '80', '130', '1'),
('186', '81', '131', '1'),
('188', '82', '133', '1'),
('189', '83', '134', '1'),
('190', '86', '135', '1'),
('192', '87', '137', '1'),
('223', '88', '141', '1'),
('257', '91', '175', '1'),
('193', '92', '1', '1'),
('194', '92', '2', '2'),
('195', '92', '4', '3'),
('196', '92', '5', '4'),
('197', '92', '6', '5'),
('198', '92', '7', '6'),
('199', '92', '8', '7'),
('200', '92', '9', '8'),
('201', '92', '10', '9'),
('202', '92', '11', '10'),
('203', '92', '12', '11'),
('204', '92', '13', '12'),
('205', '92', '14', '13'),
('206', '92', '15', '14'),
('207', '92', '26', '15'),
('208', '93', '138', '1'),
('209', '95', '139', '1'),
('211', '97', '62', '1'),
('212', '97', '63', '2'),
('213', '97', '64', '3'),
('214', '97', '65', '4'),
('215', '97', '66', '5'),
('216', '97', '67', '6'),
('217', '97', '68', '7'),
('218', '97', '69', '8'),
('219', '97', '75', '9'),
('220', '97', '71', '10'),
('221', '97', '73', '11'),
('222', '97', '74', '12'),
('253', '98', '171', '1'),
('254', '99', '172', '1'),
('255', '100', '173', '1'),
('256', '101', '174', '1'),
('258', '102', '176', '1'),
('259', '103', '177', '1'),
('264', '128', '178', '1'),
('265', '128', '179', '2'),
('266', '128', '180', '3'),
('267', '128', '181', '4'),
('271', '129', '183', '1'),
('272', '130', '184', '1'),
('274', '131', '186', '1'),
('421', '132', '1', '1'),
('422', '132', '2', '2'),
('423', '132', '4', '3'),
('424', '132', '5', '4'),
('425', '132', '182', '5'),
('426', '132', '7', '6'),
('427', '132', '188', '7'),
('428', '132', '10', '8'),
('429', '132', '15', '9'),
('430', '132', '12', '10'),
('431', '132', '13', '11'),
('432', '132', '14', '12'),
('433', '132', '11', '13'),
('434', '132', '26', '14'),
('435', '132', '59', '15'),
('436', '132', '60', '16'),
('437', '132', '39', '17'),
('438', '132', '100', '18'),
('439', '132', '102', '19'),
('440', '132', '103', '20'),
('441', '132', '104', '21'),
('442', '132', '101', '22'),
('443', '132', '76', '23'),
('444', '132', '77', '24'),
('445', '132', '78', '25'),
('379', '133', '1', '1'),
('380', '133', '2', '2'),
('381', '133', '4', '3'),
('382', '133', '5', '4'),
('383', '133', '182', '5'),
('384', '133', '7', '6'),
('385', '133', '188', '7'),
('386', '133', '10', '8'),
('387', '133', '15', '9'),
('388', '133', '12', '10'),
('389', '133', '13', '11'),
('390', '133', '14', '12'),
('391', '133', '11', '13'),
('392', '133', '26', '14'),
('393', '133', '59', '15'),
('394', '133', '61', '16'),
('395', '133', '39', '17'),
('396', '133', '100', '18'),
('397', '133', '102', '19'),
('398', '133', '103', '20'),
('399', '133', '104', '21'),
('400', '133', '101', '22');
INSERT IGNORE INTO `lab_repotests` (`id`, `mstRepoKey`, `mstTestKey`, `EOrder`) VALUES
('401', '133', '76', '23'),
('402', '133', '77', '24'),
('403', '133', '78', '25'),
('404', '133', '16', '26'),
('405', '133', '80', '27'),
('406', '133', '81', '28'),
('407', '133', '27', '29'),
('408', '133', '28', '30'),
('409', '133', '29', '31'),
('410', '133', '30', '32'),
('411', '133', '31', '33'),
('412', '133', '32', '34'),
('413', '133', '187', '35'),
('414', '133', '17', '36'),
('415', '133', '18', '37'),
('416', '133', '19', '38'),
('417', '133', '22', '39'),
('418', '133', '20', '40'),
('419', '133', '21', '41'),
('420', '133', '38', '42'),
('447', '133', '196', '43'),
('446', '134', '196', '1'),
('448', '135', '197', '1'),
('449', '136', '198', '1'),
('450', '137', '199', '1');
-- No rows found in `lab_repotests`

View File

@ -308,7 +308,7 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface
public function sharePrescriptionBundle(array $payload, string $entityId = ''): array
{
return $this->post('/v3/bundle/push', [
'consent_id' => (string) ($payload['consent_handle'] ?? $payload['consent_id'] ?? ''),
'consent_id' => (string) ($payload['consent_id'] ?? $payload['consent_handle'] ?? ''),
'hi_type' => (string) ($payload['hi_type'] ?? 'OPConsultation'),
'fhir_bundle' => $payload['bundle'] ?? $payload['fhir_bundle'] ?? [],
]);
@ -317,7 +317,7 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface
public function shareIpdDischargeBundle(array $payload, string $entityId = ''): array
{
return $this->post('/v3/bundle/push', [
'consent_id' => (string) ($payload['consent_handle'] ?? $payload['consent_id'] ?? ''),
'consent_id' => (string) ($payload['consent_id'] ?? $payload['consent_handle'] ?? ''),
'hi_type' => 'DischargeSummary',
'fhir_bundle' => $payload['bundle'] ?? $payload['fhir_bundle'] ?? [],
]);
@ -326,7 +326,7 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface
public function shareDiagnosisReportBundle(array $payload, string $entityId = ''): array
{
return $this->post('/v3/bundle/push', [
'consent_id' => (string) ($payload['consent_handle'] ?? $payload['consent_id'] ?? ''),
'consent_id' => (string) ($payload['consent_id'] ?? $payload['consent_handle'] ?? ''),
'hi_type' => 'DiagnosticReport',
'fhir_bundle' => $payload['bundle'] ?? $payload['fhir_bundle'] ?? [],
]);
@ -582,4 +582,66 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface
return $this->get('/v3/drugs/version', $query);
}
// -------------------------------------------------------------------------
// Pathology Terminology (LOINC)
// -------------------------------------------------------------------------
/**
* Fetch pathology panel masters from the Bridge API.
*
* @param string $subCategory 'PATHOLOGY' | 'BIOPSY' | '' for all
* @param int $limit items per page (max 500)
* @param int $offset pagination offset
* @param string $updatedSince ISO-8601 datetime for incremental sync
* @return array<string, mixed>
*/
public function pathologyMasters(
string $subCategory = '',
int $limit = 200,
int $offset = 0,
string $updatedSince = ''
): array {
$query = ['limit' => max(1, min(500, $limit)), 'offset' => $offset];
if ($subCategory !== '') {
$query['sub_category'] = strtoupper($subCategory);
}
if ($updatedSince !== '') {
$query['updated_since'] = $updatedSince;
}
if ($this->hfrId !== '') {
$query['hfr_id'] = $this->hfrId;
}
return $this->get('/v3/pathology/masters', $query);
}
/**
* Fetch LOINC component details for a specific pathology panel.
*
* @param string $parentTest Exact panel name (e.g. 'Complete Blood Count Panel')
* @param int $limit items per page
* @param int $offset pagination offset
* @param string $updatedSince ISO-8601 datetime for incremental sync
* @return array<string, mixed>
*/
public function pathologyComponents(
string $parentTest = '',
int $limit = 200,
int $offset = 0,
string $updatedSince = ''
): array {
$query = ['limit' => max(1, min(500, $limit)), 'offset' => $offset];
if ($parentTest !== '') {
$query['parent_test'] = $parentTest;
}
if ($updatedSince !== '') {
$query['updated_since'] = $updatedSince;
}
if ($this->hfrId !== '') {
$query['hfr_id'] = $this->hfrId;
}
return $this->get('/v3/pathology/components', $query);
}
}

View File

@ -6,22 +6,61 @@
<div class="card-body">
<div class="row">
<div class="col-md-12">
<div class="form-group">
<label>Report Name</label>
<?php
$repo_name = '';
$repo_id = '0';
$HTMLData = '';
if (count($labReport_master ?? []) > 0) {
$repo_name = $labReport_master[0]->Title ?? '';
$repo_id = $labReport_master[0]->mstRepoKey ?? '0';
$HTMLData = $labReport_master[0]->HTMLData ?? '';
}
?>
<input class="form-control" id="input_Reportname" name="input_Reportname" placeholder="Report Name" type="text" value="<?= esc($repo_name) ?>" />
<input type="hidden" id="repo_id" value="<?= esc($repo_id) ?>">
<?php
$repo_name = '';
$repo_id = '0';
$HTMLData = '';
$repo_loinc_code = $repo_loinc_code ?? '';
if (count($labReport_master ?? []) > 0) {
$repo_name = $labReport_master[0]->Title ?? '';
$repo_id = $labReport_master[0]->mstRepoKey ?? '0';
$HTMLData = $labReport_master[0]->HTMLData ?? '';
$repo_loinc_code = $labReport_master[0]->loinc_code ?? $repo_loinc_code;
}
?>
<input type="hidden" id="repo_id" value="<?= esc($repo_id) ?>">
</div>
<!-- Report Name with Bridge API Autocomplete -->
<div class="col-md-8">
<div class="form-group position-relative">
<label for="input_Reportname">
Report Name
<span id="loinc-fetch-spinner" class="spinner-border spinner-border-sm text-primary ms-2 d-none" role="status"></span>
</label>
<input class="form-control" id="input_Reportname" name="input_Reportname"
placeholder="Type to search from Bridge API…"
type="text" value="<?= esc($repo_name) ?>"
autocomplete="off" />
<!-- Autocomplete dropdown -->
<div id="report-name-suggestions"
class="list-group shadow"
style="position:absolute;z-index:1050;width:100%;display:none;max-height:260px;overflow-y:auto;"></div>
</div>
</div>
<!-- LOINC Code (auto-filled on selection, editable) -->
<div class="col-md-4">
<div class="form-group">
<label for="input_loinc_code">
LOINC Code
<small class="text-muted">(auto-filled from Bridge API)</small>
</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-flask"></i></span>
<input class="form-control" id="input_loinc_code" name="loinc_code"
placeholder="e.g. 58410-2"
type="text" value="<?= esc($repo_loinc_code) ?>" />
</div>
<div class="d-flex align-items-center gap-2 mt-2">
<button type="button" class="btn btn-outline-primary btn-sm" id="btn-load-master-template">Load Master Template</button>
<button type="button" class="btn btn-outline-success btn-sm" id="btn-apply-master-panel">Apply Panel Mapping</button>
<span id="master-template-spinner" class="spinner-border spinner-border-sm text-primary d-none" role="status"></span>
</div>
<small id="selected-master-meta" class="text-muted d-block mt-2">Select a Bridge API master row, then click "Load Master Template".</small>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label>Attach Charge Name</label>
@ -96,40 +135,401 @@
</div>
<?= form_close() ?>
<script>
$('#updatereport').click(function() {
var repo_id = $('#repo_id').val();
var input_Reportname = $('#input_Reportname').val();
var charge_id = $('#charge_id').val();
var group_id = $('#group_id').val();
var HTMLData = (typeof CKEDITOR !== 'undefined' && CKEDITOR.instances.HTMLData) ? CKEDITOR.instances.HTMLData.getData() : $('#HTMLData').val();
var csrf_value = $('input[name="<?= csrf_token() ?>"]').first().val() || '<?= csrf_hash() ?>';
(function () {
'use strict';
if (repo_id > 0) {
$.post('<?= base_url('Lab_Admin/report_update') ?>', {
"repo_id": repo_id,
"input_Reportname": input_Reportname,
"charge_id": charge_id,
"group_id": group_id,
"HTMLData": HTMLData,
"<?= csrf_token() ?>": csrf_value
}, function(data) {
const SEARCH_URL = '<?= base_url('Lab_Admin/pathology_masters_search') ?>';
const TEMPLATE_URL = '<?= base_url('Lab_Admin/pathology_master_template') ?>';
const APPLY_PANEL_URL = '<?= base_url('Lab_Admin/pathology_master_apply_panel') ?>';
const nameInput = document.getElementById('input_Reportname');
const loincInput = document.getElementById('input_loinc_code');
const groupInput = document.getElementById('group_id');
const dropdown = document.getElementById('report-name-suggestions');
const spinner = document.getElementById('loinc-fetch-spinner');
const templateBtn = document.getElementById('btn-load-master-template');
const applyPanelBtn = document.getElementById('btn-apply-master-panel');
const templateSpinner = document.getElementById('master-template-spinner');
const masterMeta = document.getElementById('selected-master-meta');
let debounceTimer = null;
let selectedMaster = null;
// ── Autocomplete on keyup ────────────────────────────────────────────────
nameInput.addEventListener('input', function () {
clearTimeout(debounceTimer);
const q = this.value.trim();
if (q.length < 2) {
hideDropdown();
return;
}
spinner.classList.remove('d-none');
debounceTimer = setTimeout(function () {
fetch(SEARCH_URL + '?q=' + encodeURIComponent(q), {
headers: { 'X-Requested-With': 'XMLHttpRequest' }
})
.then(r => r.json())
.then(items => {
spinner.classList.add('d-none');
renderDropdown(items);
})
.catch(() => {
spinner.classList.add('d-none');
hideDropdown();
});
}, 320);
});
function renderDropdown(items) {
dropdown.innerHTML = '';
if (! items || items.length === 0) {
hideDropdown();
return;
}
items.forEach(function (item) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'list-group-item list-group-item-action d-flex justify-content-between align-items-center';
const sourceBadge = item.source === 'bridge'
? '<span class="badge bg-primary ms-1" title="From Bridge API">API</span>'
: '<span class="badge bg-secondary ms-1" title="From local database">Local</span>';
const rateBadge = item.standard_rate
? ' <span class="badge bg-warning text-dark ms-1" title="Standard Rate">Rs ' + escHtml(item.standard_rate) + '</span>'
: '';
btn.innerHTML =
'<span>' + escHtml(item.name) + ' ' + sourceBadge +
(item.sub_category ? ' <span class="badge bg-info text-dark ms-1" title="Sub Category">' + escHtml(item.sub_category) + '</span>' : '') +
rateBadge +
'</span>' +
(item.loinc_code
? '<span class="badge bg-success ms-2" title="LOINC">' + escHtml(item.loinc_code) + '</span>'
: '<span class="badge bg-light text-muted border ms-2">No LOINC</span>');
btn.addEventListener('click', function () {
nameInput.value = item.name;
loincInput.value = item.loinc_code || '';
applyGroupFromSubCategory(item.sub_category || '');
selectedMaster = item;
updateMasterMeta(item);
hideDropdown();
nameInput.focus();
});
dropdown.appendChild(btn);
});
dropdown.style.display = 'block';
}
function hideDropdown() {
dropdown.style.display = 'none';
dropdown.innerHTML = '';
}
// Close dropdown when clicking outside
document.addEventListener('click', function (e) {
if (! nameInput.contains(e.target) && ! dropdown.contains(e.target)) {
hideDropdown();
}
});
function escHtml(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function normalizeLabel(str) {
return String(str || '')
.toLowerCase()
.replace(/[^a-z0-9]+/g, '')
.trim();
}
function canonicalGroupKey(str) {
const key = normalizeLabel(str);
if (!key) return '';
const aliasMap = {
hematology: 'haematology',
haematology: 'haematology',
biochemistry: 'biochemistry',
biochem: 'biochemistry',
endocrinology: 'endocrinology',
vitaminsminerals: 'vitaminsminerals',
vitaminsandminerals: 'vitaminsminerals',
infectiousdisease: 'infectiousdisease',
microbiology: 'infectiousdisease',
inflammatorymarkers: 'inflammatorymarkers',
tumormarkers: 'tumormarkers',
clinicalpathology: 'clinicalpathology'
};
return aliasMap[key] || key;
}
function updateMasterMeta(item) {
if (!masterMeta) return;
if (!item) {
masterMeta.textContent = 'Select a Bridge API master row, then click "Load Master Template".';
return;
}
const source = item.source === 'bridge' ? 'Bridge API' : 'Local';
const name = item.display_name || item.test_name || item.name || '';
const loinc = item.loinc_code || 'NA';
const sub = item.sub_category || 'NA';
const rate = item.standard_rate ? ('Rs ' + item.standard_rate) : 'NA';
masterMeta.textContent = 'Selected: ' + name + ' | LOINC: ' + loinc + ' | Sub Category: ' + sub + ' | Rate: ' + rate + ' | Source: ' + source;
}
function applyGroupFromSubCategory(subCategory) {
if (!groupInput) return;
const wanted = canonicalGroupKey(subCategory);
const options = Array.from(groupInput.options || []);
let match = null;
if (wanted) {
match = options.find(function (opt) {
return canonicalGroupKey(opt.textContent) === wanted;
}) || null;
if (!match) {
match = options.find(function (opt) {
const txt = canonicalGroupKey(opt.textContent);
return txt.includes(wanted) || wanted.includes(txt);
}) || null;
}
}
if (!match) {
match = options.find(function (opt) {
return normalizeLabel(opt.textContent).includes('pathology');
}) || null;
}
if (match && groupInput.value !== match.value) {
groupInput.value = match.value;
groupInput.dispatchEvent(new Event('change', { bubbles: true }));
}
}
templateBtn.addEventListener('click', function () {
const panelName = (selectedMaster && (selectedMaster.test_name || selectedMaster.name))
? String(selectedMaster.test_name || selectedMaster.name).trim()
: nameInput.value.trim();
if (!panelName) {
if (typeof notify === 'function') {
notify('warning', 'Missing report name', 'Select a pathology master or enter report name first.');
}
nameInput.focus();
return;
}
templateBtn.disabled = true;
templateSpinner.classList.remove('d-none');
fetch(TEMPLATE_URL + '?parent_test=' + encodeURIComponent(panelName), {
headers: { 'X-Requested-With': 'XMLHttpRequest' }
})
.then(r => r.json())
.then(resp => {
templateBtn.disabled = false;
templateSpinner.classList.add('d-none');
if (!resp || Number(resp.ok) !== 1) {
if (typeof notify === 'function') {
notify('warning', 'Master template fetch failed', (resp && resp.error) ? resp.error : 'Unable to fetch template.');
}
return;
}
const existingHtml = (typeof CKEDITOR !== 'undefined' && CKEDITOR.instances.HTMLData)
? CKEDITOR.instances.HTMLData.getData().trim()
: String(document.getElementById('HTMLData').value || '').trim();
if (existingHtml !== '' && !confirm('Replace existing template content with gateway master template?')) {
return;
}
if (typeof CKEDITOR !== 'undefined' && CKEDITOR.instances.HTMLData) {
CKEDITOR.instances.HTMLData.setData(resp.template_html || '');
} else {
document.getElementById('HTMLData').value = resp.template_html || '';
}
if (resp.loinc_code) {
loincInput.value = resp.loinc_code;
}
if (resp.sub_category) {
applyGroupFromSubCategory(resp.sub_category);
}
selectedMaster = Object.assign({}, selectedMaster || {}, {
name: resp.panel_name || panelName,
test_name: resp.panel_name || panelName,
loinc_code: resp.loinc_code || loincInput.value,
sub_category: resp.sub_category || '',
standard_rate: resp.standard_rate || '',
source: 'bridge',
});
updateMasterMeta(selectedMaster);
if (typeof notify === 'function') {
notify('success', 'Template loaded', 'Loaded ' + String(resp.components_count || 0) + ' component rows from gateway master.');
}
})
.catch(() => {
templateBtn.disabled = false;
templateSpinner.classList.add('d-none');
if (typeof notify === 'function') {
notify('warning', 'Master template fetch failed', 'Network or server error while loading master template.');
}
});
});
applyPanelBtn.addEventListener('click', function () {
const repoId = String(document.getElementById('repo_id').value || '').trim();
const panelName = (selectedMaster && (selectedMaster.test_name || selectedMaster.name))
? String(selectedMaster.test_name || selectedMaster.name).trim()
: nameInput.value.trim();
if (!repoId || Number(repoId) <= 0) {
if (typeof notify === 'function') {
notify('warning', 'Save report first', 'Create/save report once, then apply full panel mapping.');
}
return;
}
if (!panelName) {
if (typeof notify === 'function') {
notify('warning', 'Missing report name', 'Select a pathology master or enter panel name first.');
}
nameInput.focus();
return;
}
if (!confirm('This will replace current panel test mapping and print template with gateway panel mapping. Continue?')) {
return;
}
applyPanelBtn.disabled = true;
templateSpinner.classList.remove('d-none');
const csrfTokenName = '<?= csrf_token() ?>';
const csrfTokenValue = document.querySelector('input[name="<?= csrf_token() ?>"]')?.value || '<?= csrf_hash() ?>';
const formData = new FormData();
formData.append('repo_id', repoId);
formData.append('panel_name', panelName);
formData.append('replace_existing', '1');
formData.append(csrfTokenName, csrfTokenValue);
fetch(APPLY_PANEL_URL, {
method: 'POST',
headers: { 'X-Requested-With': 'XMLHttpRequest' },
body: formData,
})
.then(r => r.json())
.then(resp => {
applyPanelBtn.disabled = false;
templateSpinner.classList.add('d-none');
if (!resp || Number(resp.ok) !== 1) {
if (typeof notify === 'function') {
notify('warning', 'Panel apply failed', (resp && resp.error) ? resp.error : 'Unable to apply panel mapping.');
}
return;
}
if (typeof CKEDITOR !== 'undefined' && CKEDITOR.instances.HTMLData) {
CKEDITOR.instances.HTMLData.setData(resp.template_html || '');
} else {
document.getElementById('HTMLData').value = resp.template_html || '';
}
if (resp.panel_loinc_code) {
loincInput.value = resp.panel_loinc_code;
}
if (typeof notify === 'function') {
notify(
'success',
'Panel mapping applied',
'Imported ' + String(resp.components_count || 0) + ' tests (created: ' + String(resp.created_tests || 0) + ', updated: ' + String(resp.updated_tests || 0) + ').'
);
}
// Reload same edit screen so report parameter list reflects imported panel components.
if (typeof load_form_div === 'function') {
load_form_div('<?= base_url('Lab_Admin/reportedit_load') ?>/' + encodeURIComponent(repoId), 'test_div');
}
})
.catch(() => {
applyPanelBtn.disabled = false;
templateSpinner.classList.add('d-none');
if (typeof notify === 'function') {
notify('warning', 'Panel apply failed', 'Network or server error while applying panel mapping.');
}
});
});
nameInput.addEventListener('change', function () {
if (selectedMaster && String(selectedMaster.name || '').trim() !== this.value.trim()) {
selectedMaster = null;
updateMasterMeta(null);
}
});
// ── Save / Insert ────────────────────────────────────────────────────────
document.getElementById('updatereport').addEventListener('click', function () {
const repo_id = document.getElementById('repo_id').value;
const name = nameInput.value.trim();
const charge_id = document.getElementById('charge_id').value;
const group_id = document.getElementById('group_id').value;
const loinc_code = loincInput.value.trim();
const HTMLData = (typeof CKEDITOR !== 'undefined' && CKEDITOR.instances.HTMLData)
? CKEDITOR.instances.HTMLData.getData()
: document.getElementById('HTMLData').value;
const csrf_token = '<?= csrf_token() ?>';
const csrf_value = document.querySelector('input[name="<?= csrf_token() ?>"]')?.value || '<?= csrf_hash() ?>';
const payload = {
repo_id,
input_Reportname: name,
charge_id,
group_id,
HTMLData,
loinc_code,
[csrf_token]: csrf_value
};
const url = repo_id > 0
? '<?= base_url('Lab_Admin/report_update') ?>'
: '<?= base_url('Lab_Admin/report_insert') ?>';
$.post(url, payload, function (data) {
if (repo_id > 0) {
if (data.showcontent && typeof notify === 'function') {
notify('success', 'Saved', data.showcontent);
}
}, 'json');
} else {
$.post('<?= base_url('Lab_Admin/report_insert') ?>', {
"repo_id": repo_id,
"input_Reportname": input_Reportname,
"charge_id": charge_id,
"group_id": group_id,
"HTMLData": HTMLData,
"<?= csrf_token() ?>": csrf_value
}, function(data) {
} else {
if (data.insertid > 0) {
load_form('<?= base_url('Lab_Admin/report_list') ?>');
}
}, 'json');
}
}
}, 'json');
});
}());
</script>

View File

@ -10,7 +10,10 @@ $Unit = '';
$FixedNormals = '';
$isGenderSpecific = 0;
$FixedNormalsWomen = '';
$checkbox_checked = '';
$loincCode = '';
$loincProperty = '';
$loincSystem = '';
$loincScale = '';
if (count($lab_test_parameter ?? []) > 0) {
$mstTestKey = $lab_test_parameter[0]->mstTestKey ?? 0;
@ -24,241 +27,407 @@ if (count($lab_test_parameter ?? []) > 0) {
$FixedNormals = $lab_test_parameter[0]->FixedNormals ?? '';
$isGenderSpecific = (int) ($lab_test_parameter[0]->isGenderSpecific ?? 0);
$FixedNormalsWomen = $lab_test_parameter[0]->FixedNormalsWomen ?? '';
$loincCode = $lab_test_parameter[0]->loinc_code ?? '';
$loincProperty = $lab_test_parameter[0]->loinc_property ?? '';
$loincSystem = $lab_test_parameter[0]->loinc_system ?? '';
$loincScale = $lab_test_parameter[0]->loinc_scale ?? '';
}
if ($isGenderSpecific == 1) {
$checkbox_checked = 'checked';
}
$normalMin = '';
$normalMax = '';
if (strpos((string) $FixedNormals, '-') !== false) {
[$normalMin, $normalMax] = array_pad(explode('-', (string) $FixedNormals, 2), 2, '');
}
$femaleNormalMin = '';
$femaleNormalMax = '';
if (strpos((string) $FixedNormalsWomen, '-') !== false) {
[$femaleNormalMin, $femaleNormalMax] = array_pad(explode('-', (string) $FixedNormalsWomen, 2), 2, '');
}
$valueType = 'TEXT';
if ($Formula !== '') {
$valueType = 'FORMULA';
} elseif ($VRule !== '' || ($normalMin !== '' || $normalMax !== '')) {
$valueType = 'NUMERIC';
}
?>
<?= form_open() ?>
<div class="card admin-card">
<div class="card admin-card component-rule-card">
<style>
.component-rule-card {
border: 0;
box-shadow: none;
}
.component-rule-card .card-header {
padding: 12px 16px;
}
.component-rule-card .card-header h3 {
margin: 0;
font-size: 18px;
font-weight: 500;
color: #567299;
}
.component-rule-card .card-body {
padding: 16px 18px 12px;
}
.component-rule-card .form-label {
margin-bottom: 6px;
font-size: 13px;
font-weight: 600;
color: #567299;
}
.component-rule-card .form-control,
.component-rule-card .form-select {
height: 38px;
font-size: 14px;
}
.component-rule-card textarea.form-control {
height: auto;
min-height: 64px;
}
.component-rule-grid {
display: grid;
grid-template-columns: 170px 1fr 170px 1fr;
gap: 12px 14px;
align-items: center;
}
.component-rule-grid .wide {
grid-column: span 3;
}
.component-rule-preset {
display: inline-flex;
gap: 10px;
flex-wrap: wrap;
}
.component-rule-preset .btn {
font-size: 13px;
padding: 6px 10px;
}
.component-rule-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
padding-top: 14px;
margin-top: 18px;
border-top: 1px solid #e6edf5;
}
.component-rule-hint {
font-size: 12px;
color: #7a8797;
}
.component-gateway-meta {
font-size: 12px;
color: #5d738f;
margin-top: 4px;
line-height: 1.5;
}
@media (max-width: 991.98px) {
.component-rule-grid {
grid-template-columns: 1fr;
}
.component-rule-grid .wide {
grid-column: auto;
}
}
</style>
<div class="card-header bg-white">
<h3 class="mb-0">Test Parameter</h3>
<h3>Edit Component Rule</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label>Name of Test</label>
<input class="form-control" id="input_Test_name" placeholder="Test Name" value="<?= esc($Test) ?>" type="text" autocomplete="off">
<input type="hidden" id="mstTestKey" value="<?= esc($mstTestKey) ?>">
<input type="hidden" id="mstRepoKey" value="<?= esc($mstRepoKey ?? 0) ?>">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label>Test code</label>
<input class="form-control" id="input_test_code" placeholder="Test code" value="<?= esc($TestID) ?>" type="text" autocomplete="off">
<input type="hidden" id="mstTestKey" value="<?= esc($mstTestKey) ?>">
<input type="hidden" id="mstRepoKey" value="<?= esc($mstRepoKey ?? 0) ?>">
<input type="hidden" id="input_test_code" value="<?= esc($TestID) ?>">
<input type="hidden" id="input_Formula" value="<?= esc($Formula) ?>">
<input type="hidden" id="input_loinc_property" value="<?= esc($loincProperty) ?>">
<input type="hidden" id="input_loinc_system" value="<?= esc($loincSystem) ?>">
<input type="hidden" id="input_loinc_scale" value="<?= esc($loincScale) ?>">
<div class="component-rule-grid">
<label for="input_Test_name" class="form-label text-end">Component</label>
<input class="form-control wide" id="input_Test_name" value="<?= esc($Test) ?>" type="text" autocomplete="off">
<label for="input_loinc_code" class="form-label text-end">LOINC Code</label>
<div>
<div class="d-flex gap-2">
<input class="form-control" id="input_loinc_code" value="<?= esc($loincCode) ?>" type="text" autocomplete="off">
<button type="button" class="btn btn-outline-primary" id="btn_load_gateway_component">Load From API</button>
</div>
<div id="gateway_meta_text" class="component-gateway-meta">Property: <?= esc($loincProperty ?: '-') ?> | System: <?= esc($loincSystem ?: '-') ?> | Scale: <?= esc($loincScale ?: '-') ?></div>
</div>
<label for="input_Unit" class="form-label text-end">Units</label>
<input class="form-control" id="input_Unit" placeholder="e.g. g/dL" value="<?= esc($Unit) ?>" type="text" autocomplete="off">
<label for="rule_value_type" class="form-label text-end">Value Type</label>
<select class="form-select" id="rule_value_type">
<option value="NUMERIC" <?= $valueType === 'NUMERIC' ? 'selected' : '' ?>>NUMERIC</option>
<option value="TEXT" <?= $valueType === 'TEXT' ? 'selected' : '' ?>>TEXT</option>
<option value="FORMULA" <?= $valueType === 'FORMULA' ? 'selected' : '' ?>>FORMULA</option>
</select>
<label class="form-label text-end">Gender Specific</label>
<div>
<label class="form-check-label"><input id="chk_isGenderSpecific" class="form-check-input me-2" type="checkbox" <?= $isGenderSpecific === 1 ? 'checked' : '' ?>>Use separate female normal range</label>
</div>
<label for="input_Default" class="form-label text-end">Default Value</label>
<input class="form-control wide" id="input_Default" placeholder="Default result value" value="<?= esc($Result) ?>" type="text" autocomplete="off">
<label for="input_Validation" class="form-label text-end">Validation Rule</label>
<input class="form-control wide" id="input_Validation" placeholder="e.g. numeric|min:0|max:100" value="<?= esc($VRule) ?>" type="text" autocomplete="off">
<label class="form-label text-end">Quick Preset</label>
<div class="wide">
<div class="component-rule-preset">
<button type="button" class="btn btn-outline-secondary js-rule-preset" data-rule="numeric|min:0|max:100">CBC Numeric</button>
<button type="button" class="btn btn-outline-secondary js-rule-preset" data-rule="numeric|min:0|max:1000">LFT Numeric</button>
<button type="button" class="btn btn-outline-secondary js-rule-preset" data-rule="numeric|min:0|max:10000">Hormone Numeric</button>
</div>
</div>
<label for="normal_min" class="form-label text-end">Normal Min</label>
<input class="form-control" id="normal_min" placeholder="e.g. 12.0" value="<?= esc($normalMin) ?>" type="text" autocomplete="off">
<label for="normal_max" class="form-label text-end">Normal Max</label>
<input class="form-control" id="normal_max" placeholder="e.g. 15.5" value="<?= esc($normalMax) ?>" type="text" autocomplete="off">
<label for="female_normal_min" class="form-label text-end">Female Normal Min</label>
<input class="form-control" id="female_normal_min" placeholder="optional" value="<?= esc($femaleNormalMin) ?>" type="text" autocomplete="off">
<label for="female_normal_max" class="form-label text-end">Female Normal Max</label>
<input class="form-control" id="female_normal_max" placeholder="optional" value="<?= esc($femaleNormalMax) ?>" type="text" autocomplete="off">
<label for="input_Message" class="form-label text-end">Notes</label>
<textarea class="form-control wide" id="input_Message" placeholder="Adult male, fasting, etc."><?= esc($VMsg) ?></textarea>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label>Default Value</label>
<input class="form-control" id="input_Default" placeholder="Default Value" value="<?= esc($Result) ?>" type="text" autocomplete="off">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label>Formula</label>
<input class="form-control" id="input_Formula" placeholder="Formula" value="<?= esc($Formula) ?>" type="text" autocomplete="off">
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label>Validation Rule</label>
<input class="form-control" id="input_Validation" placeholder="Validation Rule" value="<?= esc($VRule) ?>" type="text" autocomplete="off">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label>Unit</label>
<input class="form-control" id="input_Unit" placeholder="Unit" value="<?= esc($Unit) ?>" type="text" autocomplete="off">
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label>Message</label>
<input class="form-control" id="input_Message" placeholder="Message" value="<?= esc($VMsg) ?>" type="text" autocomplete="off">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label>Fixed Normals</label>
<input class="form-control" id="input_Fixed" placeholder="Fixed Normals" value="<?= esc($FixedNormals) ?>" type="text" autocomplete="off">
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label><input id="chk_isGenderSpecific" name="chk_isGenderSpecific" type="checkbox" <?= $checkbox_checked ?>> Is Gender Specific</label>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label>Fixed Normals For Women</label>
<input class="form-control" id="input_FixedNormalsWomen" placeholder="Fixed Normals For Women" value="<?= esc($FixedNormalsWomen) ?>" type="text" autocomplete="off">
</div>
</div>
</div>
<hr />
<div class="row">
<div class="col-md-12" id="div_option_list">
<?= view('PathLab_Report/_test_option_table', ['lab_test_option' => $lab_test_option ?? [], 'mstTestKey' => $mstTestKey]) ?>
</div>
<div class="col-md-12">
<div class="col-md-4">
<div class="form-group">
<label>Code</label>
<input class="form-control" id="input_op_value" name="input_op_value" placeholder="Test Name" value="" type="text" autocomplete="off">
</div>
</div>
<div class="col-md-2">
<div class="form-group">
<label><input id="chk_bold" name="chk_bold" type="checkbox"> Bold</label>
</div>
</div>
<div class="col-md-2">
<div class="form-group">
<label>&nbsp;</label>
<button type="button" class="btn btn-primary" id="btn_add_option">Add Option</button>
</div>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-md-4">
<div class="form-group">
<label>&nbsp;</label>
<button type="button" class="btn btn-primary" id="btn_item_update">Update</button>
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label>&nbsp;</label>
<button onclick="load_form_div('<?= base_url('Lab_Admin/report_test_list') ?>/<?= esc($mstRepoKey ?? 0) ?>','test_div');" type="button" class="btn btn-outline-primary">Test List</button>
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label>&nbsp;</label>
<button onclick="load_form_div('<?= base_url('Lab_Admin/test_parameter_load') ?>/0/<?= esc($mstRepoKey ?? 0) ?>','test_div');" type="button" class="btn btn-outline-secondary">Add New Test</button>
</div>
</div>
<div class="component-rule-hint mt-2">Normal range is stored as `min-max`. Female normal range is used only when gender-specific range is enabled.</div>
<div class="component-rule-actions">
<button type="button" class="btn btn-primary" id="btn_item_update">Save Rule</button>
<button type="button" class="btn btn-outline-secondary" id="btn_component_close">Close</button>
</div>
</div>
</div>
<?= form_close() ?>
<script>
$('#btn_item_update').click(function() {
var input_Test_name = $('#input_Test_name').val();
var input_test_code = $('#input_test_code').val();
var input_Default = $('#input_Default').val();
var input_Formula = $('#input_Formula').val();
var input_Validation = $('#input_Validation').val();
var input_Unit = $('#input_Unit').val();
var input_Message = $('#input_Message').val();
var input_Fixed = $('#input_Fixed').val();
var input_FixedNormalsWomen = $('#input_FixedNormalsWomen').val();
var mstTestKey = $('#mstTestKey').val();
var mstRepoKey = $('#mstRepoKey').val();
var csrf_value = $('input[name="<?= csrf_token() ?>"]').first().val() || '<?= csrf_hash() ?>';
var isChecked = $('#chk_isGenderSpecific').is(':checked') ? 1 : 0;
(function () {
var GATEWAY_COMPONENT_SEARCH_URL = '<?= base_url('Lab_Admin/pathology_component_masters_search') ?>';
if (mstTestKey > 0) {
$.post('<?= base_url('Lab_Admin/test_parameter_edit') ?>', {
"input_Test_name": input_Test_name,
"input_test_code": input_test_code,
"input_Default": input_Default,
"input_Formula": input_Formula,
"input_Validation": input_Validation,
"input_Unit": input_Unit,
"input_Message": input_Message,
"input_Fixed": input_Fixed,
"input_isChecked": isChecked,
"input_FixedNormalsWomen": input_FixedNormalsWomen,
"mstTestKey": mstTestKey,
"mstRepoKey": mstRepoKey,
"<?= csrf_token() ?>": csrf_value
}, function(data) {
function buildRangeValue(minValue, maxValue) {
var min = String(minValue || '').trim();
var max = String(maxValue || '').trim();
if (min === '' && max === '') {
return '';
}
return min + '-' + max;
}
function setGatewayMeta(item) {
var p = document.getElementById('input_loinc_property');
var s = document.getElementById('input_loinc_system');
var sc = document.getElementById('input_loinc_scale');
var text = document.getElementById('gateway_meta_text');
var property = String((item && item.property) || '').trim();
var system = String((item && item.specimen_system) || '').trim();
var scale = String((item && item.scale_type) || '').trim();
if (p) p.value = property;
if (s) s.value = system;
if (sc) sc.value = scale;
if (text) {
text.textContent = 'Property: ' + (property || '-') + ' | System: ' + (system || '-') + ' | Scale: ' + (scale || '-');
}
}
function normalizeName(v) {
return String(v || '').toLowerCase().replace(/[^a-z0-9]+/g, '').trim();
}
function pickBestMatch(items, targetName, targetCode) {
if (!Array.isArray(items) || items.length === 0) {
return null;
}
var wantedName = normalizeName(targetName);
var wantedCode = String(targetCode || '').trim().toUpperCase();
var first = items[0];
for (var i = 0; i < items.length; i += 1) {
var item = items[i] || {};
var itemCode = String(item.code || '').trim().toUpperCase();
if (wantedCode !== '' && itemCode === wantedCode) {
return item;
}
}
for (var j = 0; j < items.length; j += 1) {
var it = items[j] || {};
if (normalizeName(it.name || '') === wantedName) {
return it;
}
}
return first;
}
function loadGatewayComponentMeta(silent) {
var nameInput = document.getElementById('input_Test_name');
var codeInput = document.getElementById('input_loinc_code');
var unitInput = document.getElementById('input_Unit');
var btn = document.getElementById('btn_load_gateway_component');
var q = String((nameInput && nameInput.value) || '').trim();
if (q.length < 2) {
return;
}
if (btn) btn.disabled = true;
fetch(GATEWAY_COMPONENT_SEARCH_URL + '?q=' + encodeURIComponent(q), {
headers: { 'X-Requested-With': 'XMLHttpRequest' }
})
.then(function (r) { return r.json(); })
.then(function (resp) {
if (btn) btn.disabled = false;
if (!resp || Number(resp.ok || 0) !== 1) {
if (!silent && typeof notify === 'function') {
notify('warning', 'Gateway lookup failed', (resp && resp.error) ? resp.error : 'Unable to fetch component metadata.');
}
return;
}
var match = pickBestMatch(resp.items || [], q, codeInput ? codeInput.value : '');
if (!match) {
if (!silent && typeof notify === 'function') {
notify('warning', 'No match found', 'No matching component metadata found in gateway.');
}
return;
}
if (codeInput && !String(codeInput.value || '').trim() && match.code) {
codeInput.value = String(match.code || '').trim();
}
if (unitInput && !String(unitInput.value || '').trim() && match.unit) {
unitInput.value = String(match.unit || '').trim();
}
setGatewayMeta(match);
if (!silent && typeof notify === 'function') {
notify('success', 'Gateway metadata loaded', 'Component rule helper fields filled from API.');
}
})
.catch(function () {
if (btn) btn.disabled = false;
if (!silent && typeof notify === 'function') {
notify('warning', 'Gateway lookup failed', 'Network/server error while fetching metadata.');
}
});
}
var gatewayBtn = document.getElementById('btn_load_gateway_component');
if (gatewayBtn) {
gatewayBtn.addEventListener('click', function () {
loadGatewayComponentMeta(false);
});
}
// Auto-load helper metadata when opening edit rule.
loadGatewayComponentMeta(true);
document.querySelectorAll('.js-rule-preset').forEach(function (btn) {
btn.addEventListener('click', function () {
var rule = String(btn.getAttribute('data-rule') || '').trim();
var ruleInput = document.getElementById('input_Validation');
var valueType = document.getElementById('rule_value_type');
if (ruleInput) {
ruleInput.value = rule;
}
if (valueType) {
valueType.value = 'NUMERIC';
}
});
});
document.getElementById('btn_component_close').addEventListener('click', function () {
load_form_div('<?= base_url('Lab_Admin/report_test_list') ?>/<?= esc($mstRepoKey ?? 0) ?>', 'test_div');
});
document.getElementById('btn_item_update').addEventListener('click', function () {
var input_Test_name = document.getElementById('input_Test_name').value;
var input_test_code = document.getElementById('input_test_code').value;
var input_loinc_code = document.getElementById('input_loinc_code').value;
var input_Default = document.getElementById('input_Default').value;
var input_Formula = document.getElementById('input_Formula').value;
var input_Validation = document.getElementById('input_Validation').value;
var input_Unit = document.getElementById('input_Unit').value;
var input_loinc_property = document.getElementById('input_loinc_property').value;
var input_loinc_system = document.getElementById('input_loinc_system').value;
var input_loinc_scale = document.getElementById('input_loinc_scale').value;
var input_Message = document.getElementById('input_Message').value;
var input_Fixed = buildRangeValue(document.getElementById('normal_min').value, document.getElementById('normal_max').value);
var input_FixedNormalsWomen = buildRangeValue(document.getElementById('female_normal_min').value, document.getElementById('female_normal_max').value);
var mstTestKey = document.getElementById('mstTestKey').value;
var mstRepoKey = document.getElementById('mstRepoKey').value;
var csrf_value = $('input[name="<?= csrf_token() ?>"]').first().val() || '<?= csrf_hash() ?>';
var isChecked = document.getElementById('chk_isGenderSpecific').checked ? 1 : 0;
var payload = {
"input_Test_name": input_Test_name,
"input_test_code": input_test_code,
"input_loinc_code": input_loinc_code,
"input_Default": input_Default,
"input_Formula": input_Formula,
"input_Validation": input_Validation,
"input_Unit": input_Unit,
"input_loinc_property": input_loinc_property,
"input_loinc_system": input_loinc_system,
"input_loinc_scale": input_loinc_scale,
"input_Message": input_Message,
"input_Fixed": input_Fixed,
"input_isChecked": isChecked,
"input_FixedNormalsWomen": input_FixedNormalsWomen,
"mstTestKey": mstTestKey,
"mstRepoKey": mstRepoKey,
"<?= csrf_token() ?>": csrf_value
};
if (Number(mstTestKey) > 0) {
$.post('<?= base_url('Lab_Admin/test_parameter_edit') ?>', payload, function (data) {
if (data.showcontent && typeof notify === 'function') {
notify('success', 'Saved', data.showcontent);
}
load_form_div('<?= base_url('Lab_Admin/report_test_list') ?>/<?= esc($mstRepoKey ?? 0) ?>', 'test_div');
}, 'json');
} else {
$.post('<?= base_url('Lab_Admin/test_parameter_add') ?>', {
"input_Test_name": input_Test_name,
"input_test_code": input_test_code,
"input_Default": input_Default,
"input_Formula": input_Formula,
"input_Validation": input_Validation,
"input_Unit": input_Unit,
"input_Message": input_Message,
"input_Fixed": input_Fixed,
"mstTestKey": mstTestKey,
"mstRepoKey": mstRepoKey,
"<?= csrf_token() ?>": csrf_value
}, function(data) {
if (data.insert_id > 0) {
$('#mstTestKey').val(data.insert_id);
$.post('<?= base_url('Lab_Admin/test_parameter_add') ?>', payload, function (data) {
if (Number(data.insert_id || 0) > 0) {
if (typeof notify === 'function') {
notify('success', 'Saved', data.showcontent || 'Data Saved successfully');
}
load_form_div('<?= base_url('Lab_Admin/report_test_list') ?>/<?= esc($mstRepoKey ?? 0) ?>', 'test_div');
}
}, 'json');
}
});
$('#btn_add_option').click(function() {
var input_op_value = $('#input_op_value').val();
var chk_bold = $('#chk_bold').is(':checked') ? 1 : 0;
var mstTestKey = $('#mstTestKey').val();
var csrf_value = $('input[name="<?= csrf_token() ?>"]').first().val() || '<?= csrf_hash() ?>';
if (mstTestKey > 0) {
$.post('<?= base_url('Lab_Admin/test_parameter_option_add') ?>', {
"input_op_value": input_op_value,
"chk_bold": chk_bold,
"mstTestKey": mstTestKey,
"<?= csrf_token() ?>": csrf_value
}, function(data) {
if (data.insert_id > 0) {
$('#div_option_list').html(data.option_content);
}
}, 'json');
} else {
alert('First create test');
}
});
function sortchange(mstTestKey, option_current, sort_current, option_prev, sort_prev) {
var postStr = '<?= base_url('Lab_Admin/change_sort') ?>/' + mstTestKey + '/' + option_current + '/' + sort_current + '/' + option_prev + '/' + sort_prev;
var csrf_value = $('input[name="<?= csrf_token() ?>"]').first().val() || '<?= csrf_hash() ?>';
$.post(postStr, {
"mstTestKey": mstTestKey,
"<?= csrf_token() ?>": csrf_value
}, function(data) {
if (data.insert_id > 0) {
$('#div_option_list').html(data.option_content);
}
}, 'json');
}
function remove_option(option_id, mstTestKey) {
var postStr = '<?= base_url('Lab_Admin/remove_test_option') ?>/' + option_id + '/' + mstTestKey;
var csrf_value = $('input[name="<?= csrf_token() ?>"]').first().val() || '<?= csrf_hash() ?>';
$.post(postStr, {
"mstTestKey": mstTestKey,
"<?= csrf_token() ?>": csrf_value
}, function(data) {
if (data.insert_id > 0) {
$('#div_option_list').html(data.option_content);
}
}, 'json');
}
}());
</script>

File diff suppressed because it is too large Load Diff

View File

@ -1,80 +1,474 @@
<?= form_open() ?>
<div class="card admin-card">
<div class="card-header bg-white d-flex align-items-center justify-content-between">
<h3 class="mb-0">Test List</h3>
<button onclick="load_form_div('<?= base_url('Lab_Admin/test_search_page') ?>/<?= esc($mstRepoKey ?? 0) ?>','test_div');" type="button" class="btn btn-primary btn-sm">Add New Test</button>
<div id="test_div">
<div class="card admin-card component-attach-card">
<style>
.component-attach-card .card-header h3 {
font-size: 18px;
line-height: 1.2;
font-weight: 500;
color: #567299;
}
.component-panel-pill {
display: inline-block;
padding: 6px 12px;
border-radius: 3px;
background: #2f72b1;
color: #fff;
font-size: 14px;
font-weight: 600;
}
.component-panel-pill .label {
opacity: 0.95;
margin-right: 6px;
}
.component-attach-toolbar {
position: relative;
display: grid;
grid-template-columns: 1fr 160px;
gap: 14px;
align-items: center;
margin-top: 14px;
margin-bottom: 14px;
}
.component-attach-toolbar .form-control,
.component-attach-toolbar .btn {
height: 46px;
font-size: 14px;
border-radius: 2px;
}
#gatewayComponentSuggestions {
position: absolute;
left: 0;
right: 174px;
top: 48px;
z-index: 10;
display: none;
max-height: 280px;
overflow-y: auto;
border: 1px solid #d5dde8;
background: #fff;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
}
#gatewayComponentSuggestions .list-group-item {
cursor: pointer;
font-size: 13px;
}
#gatewayComponentSuggestions .list-group-item.active,
#gatewayComponentSuggestions .list-group-item:focus {
background: #e8f1fb;
border-color: #b8d0ea;
color: #1f3f66;
outline: none;
}
.component-attach-table th,
.component-attach-table td {
font-size: 13px;
vertical-align: middle;
}
.component-attach-table th {
color: #6c7f96;
}
.component-attach-table .col-number {
width: 64px;
}
.component-attach-table .col-action {
width: 280px;
}
.component-action-stack {
display: inline-flex;
align-items: center;
gap: 8px;
flex-wrap: nowrap;
}
.component-action-stack .btn {
border-radius: 4px;
padding: 5px 10px;
font-size: 12px;
line-height: 1.2;
}
.component-action-stack .btn-arrow {
min-width: 40px;
padding: 4px 8px;
font-size: 10px;
}
.component-attach-footer {
margin-top: 18px;
text-align: right;
}
.component-attach-footer .btn {
min-width: 90px;
font-size: 12px;
}
.component-attach-card .btn {
font-size: 12px;
}
.component-attach-card .form-control {
font-size: 14px;
}
@media (max-width: 767.98px) {
.component-attach-card .card-header h3 {
font-size: 22px;
}
.component-panel-pill {
font-size: 15px;
}
.component-attach-toolbar {
grid-template-columns: 1fr;
}
.component-attach-toolbar .form-control,
.component-attach-toolbar .btn {
height: auto;
font-size: 16px;
}
#gatewayComponentSuggestions {
right: 0;
top: 44px;
}
.component-attach-table th,
.component-attach-table td,
.component-action-stack .btn {
font-size: 13px;
}
}
</style>
<div class="card-header bg-white">
<h3 class="mb-0">Add Components to Panel</h3>
</div>
<div class="card-body">
<table id="example2" class="table table-striped table-hover align-middle TableData">
<thead class="table-light">
<tr>
<th>#</th>
<th>Test Name</th>
<th>Test Code</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<?php for ($i = 0; $i < count($lab_Rep_Item_List ?? []); ++$i) { ?>
<span class="component-panel-pill"><span class="label">Panel:</span> <?= esc($panel_name ?? '') ?></span>
<div class="component-attach-toolbar">
<input type="text" class="form-control" id="componentSearchInput" placeholder="Search component (name / short / code)">
<button id="btnGatewayAdd" type="button" class="btn btn-primary">+ Add</button>
<div id="gatewayComponentSuggestions" class="list-group"></div>
</div>
<div class="table-responsive">
<table id="componentAttachTable" class="table table-bordered table-hover align-middle component-attach-table mb-0">
<thead class="table-light">
<tr>
<td><?= $i + 1 ?></td>
<td><?= esc($lab_Rep_Item_List[$i]->Test ?? '') ?></td>
<td><?= esc($lab_Rep_Item_List[$i]->TestID ?? '') ?></td>
<td>
<div class="btn-group-horizontal">
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="load_form_div('<?= base_url('Lab_Admin/test_parameter_load') ?>/<?= esc($lab_Rep_Item_List[$i]->mstTestKey ?? 0) ?>/<?= esc($mstRepoKey ?? 0) ?>','test_div');">
Edit
</button>
<button type="button" class="btn btn-outline-danger btn-sm" onclick="remove_item('<?= esc($mstRepoKey ?? 0) ?>','<?= esc($lab_Rep_Item_List[$i]->mstTestKey ?? 0) ?>');">
Remove
</button>
<?php
$option_current = $lab_Rep_Item_List[$i]->id ?? 0;
$sort_current = $lab_Rep_Item_List[$i]->EOrder ?? 0;
if ($i + 1 < count($lab_Rep_Item_List ?? [])) {
$option_next = $lab_Rep_Item_List[$i + 1]->id ?? 0;
$sort_next = $lab_Rep_Item_List[$i + 1]->EOrder ?? 0;
echo '<button type="button" class="btn btn-outline-primary btn-sm" onclick="sortchange(' . (int) ($mstRepoKey ?? 0) . ',' . (int) $option_current . ',' . (int) $sort_current . ',' . (int) $option_next . ',' . (int) $sort_next . ')">Down</button>';
}
if ($i > 0) {
$option_prev = $lab_Rep_Item_List[$i - 1]->id ?? 0;
$sort_prev = $lab_Rep_Item_List[$i - 1]->EOrder ?? 0;
echo '<button type="button" class="btn btn-outline-primary btn-sm" onclick="sortchange(' . (int) ($mstRepoKey ?? 0) . ',' . (int) $option_current . ',' . (int) $sort_current . ',' . (int) $option_prev . ',' . (int) $sort_prev . ')">Up</button>';
}
?>
</div>
</td>
<th class="col-number">#</th>
<th>Component Name</th>
<th>Short</th>
<th>Code</th>
<th class="col-action">Action</th>
</tr>
<?php } ?>
</tbody>
</table>
</thead>
<tbody>
<?php for ($i = 0; $i < count($lab_Rep_Item_List ?? []); ++$i) { ?>
<?php
$option_current = $lab_Rep_Item_List[$i]->id ?? 0;
$sort_current = $lab_Rep_Item_List[$i]->EOrder ?? 0;
$shortCode = (string) ($lab_Rep_Item_List[$i]->short_name ?? $lab_Rep_Item_List[$i]->TestID ?? '');
$componentCode = (string) ($lab_Rep_Item_List[$i]->component_code ?? '');
?>
<tr>
<td><?= $i + 1 ?></td>
<td><?= esc($lab_Rep_Item_List[$i]->Test ?? '') ?></td>
<td><?= esc($shortCode) ?></td>
<td><?= esc($componentCode) ?></td>
<td>
<div class="component-action-stack">
<?php if ($i > 0) {
$option_prev = $lab_Rep_Item_List[$i - 1]->id ?? 0;
$sort_prev = $lab_Rep_Item_List[$i - 1]->EOrder ?? 0;
echo '<button type="button" class="btn btn-outline-secondary btn-arrow" title="Move Up" onclick="sortchange(' . (int) ($mstRepoKey ?? 0) . ',' . (int) $option_current . ',' . (int) $sort_current . ',' . (int) $option_prev . ',' . (int) $sort_prev . ')">▲</button>';
} else {
echo '<button type="button" class="btn btn-outline-secondary btn-arrow disabled" title="Move Up">▲</button>';
} ?>
<?php if ($i + 1 < count($lab_Rep_Item_List ?? [])) {
$option_next = $lab_Rep_Item_List[$i + 1]->id ?? 0;
$sort_next = $lab_Rep_Item_List[$i + 1]->EOrder ?? 0;
echo '<button type="button" class="btn btn-outline-secondary btn-arrow" title="Move Down" onclick="sortchange(' . (int) ($mstRepoKey ?? 0) . ',' . (int) $option_current . ',' . (int) $sort_current . ',' . (int) $option_next . ',' . (int) $sort_next . ')">▼</button>';
} else {
echo '<button type="button" class="btn btn-outline-secondary btn-arrow disabled" title="Move Down">▼</button>';
} ?>
<button type="button" class="btn btn-warning text-white" onclick="load_form_div('<?= base_url('Lab_Admin/test_parameter_load') ?>/<?= esc($lab_Rep_Item_List[$i]->mstTestKey ?? 0) ?>/<?= esc($mstRepoKey ?? 0) ?>','test_div');">Edit</button>
<button type="button" class="btn btn-danger" onclick="remove_item('<?= esc($mstRepoKey ?? 0) ?>','<?= esc($lab_Rep_Item_List[$i]->mstTestKey ?? 0) ?>');">Remove</button>
</div>
</td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
<div class="component-attach-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<?= form_close() ?>
<script>
function sortchange(mstRepoKey, option_current, sort_current, option_prev, sort_prev) {
var postStr = '<?= base_url('Lab_Admin/change_sort_item') ?>/' + mstRepoKey + '/' + option_current + '/' + sort_current + '/' + option_prev + '/' + sort_prev;
var csrfValue = $('input[name="<?= csrf_token() ?>"]').first().val() || '<?= csrf_hash() ?>';
$.post(postStr, {
"mstRepoKey": mstRepoKey,
"<?= csrf_token() ?>": csrfValue
}, function(data) {
$('#test_div').html(data);
});
}
(function () {
var GATEWAY_COMPONENT_SEARCH_URL = '<?= base_url('Lab_Admin/pathology_component_masters_search') ?>';
var GATEWAY_COMPONENT_ADD_URL = '<?= base_url('Lab_Admin/pathology_master_add_component') ?>';
var RELOAD_COMPONENT_LIST_URL = '<?= base_url('Lab_Admin/report_test_list') ?>/';
var globalScope = window;
function remove_item(mstRepoKey, mstTestKey) {
var postStr = '<?= base_url('Lab_Admin/remove_test_item') ?>/' + mstRepoKey + '/' + mstTestKey;
var csrfValue = $('input[name="<?= csrf_token() ?>"]').first().val() || '<?= csrf_hash() ?>';
$.post(postStr, {
"mstRepoKey": mstRepoKey,
"<?= csrf_token() ?>": csrfValue
}, function() {
load_form_div('<?= base_url('Lab_Admin/report_test_list') ?>/' + mstRepoKey, 'test_div');
});
}
window.sortchange = function (mstRepoKey, option_current, sort_current, option_prev, sort_prev) {
var postStr = '<?= base_url('Lab_Admin/change_sort_item') ?>/' + mstRepoKey + '/' + option_current + '/' + sort_current + '/' + option_prev + '/' + sort_prev;
var csrfValue = $('input[name="<?= csrf_token() ?>"]').first().val() || '<?= csrf_hash() ?>';
$.post(postStr, {
"mstRepoKey": mstRepoKey,
"<?= csrf_token() ?>": csrfValue
}, function(data) {
$('#test_div').html(data);
});
};
if (window.jQuery && $.fn && $.fn.DataTable) {
$('#example2').DataTable();
}
window.remove_item = function (mstRepoKey, mstTestKey) {
var postStr = '<?= base_url('Lab_Admin/remove_test_item') ?>/' + mstRepoKey + '/' + mstTestKey;
var csrfValue = $('input[name="<?= csrf_token() ?>"]').first().val() || '<?= csrf_hash() ?>';
$.post(postStr, {
"mstRepoKey": mstRepoKey,
"<?= csrf_token() ?>": csrfValue
}, function() {
load_form_div(RELOAD_COMPONENT_LIST_URL + mstRepoKey, 'test_div');
});
};
var input = document.getElementById('componentSearchInput');
var addBtn = document.getElementById('btnGatewayAdd');
var box = document.getElementById('gatewayComponentSuggestions');
if (!input || !addBtn || !box) {
return;
}
var selected = null;
var searchTimer = null;
var suggestionItems = [];
var activeIndex = -1;
function escHtml(str) {
return String(str || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function hideSuggestionBox() {
box.style.display = 'none';
box.innerHTML = '';
suggestionItems = [];
activeIndex = -1;
}
function setActiveSuggestion(nextIndex) {
if (!suggestionItems.length) {
activeIndex = -1;
return;
}
if (nextIndex < 0) {
nextIndex = suggestionItems.length - 1;
}
if (nextIndex >= suggestionItems.length) {
nextIndex = 0;
}
suggestionItems.forEach(function (btn, index) {
btn.classList.toggle('active', index === nextIndex);
});
activeIndex = nextIndex;
if (suggestionItems[activeIndex] && typeof suggestionItems[activeIndex].scrollIntoView === 'function') {
suggestionItems[activeIndex].scrollIntoView({ block: 'nearest' });
}
}
function chooseSuggestion(item) {
selected = item;
input.value = String(item.name || '').trim();
hideSuggestionBox();
}
function renderSuggestionBox(items) {
box.innerHTML = '';
suggestionItems = [];
activeIndex = -1;
if (!items || items.length === 0) {
hideSuggestionBox();
return;
}
items.forEach(function (item) {
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'list-group-item list-group-item-action';
btn.innerHTML =
'<div><strong>' + escHtml(item.name || '') + '</strong></div>'
+ '<div class="text-muted">Short: ' + escHtml(item.short_name || '-')
+ ' | Code: ' + escHtml(item.code || '-') + '</div>';
btn.addEventListener('click', function () {
chooseSuggestion(item);
});
box.appendChild(btn);
suggestionItems.push(btn);
});
box.style.display = 'block';
setActiveSuggestion(0);
}
if (!globalScope.__pathologyComponentModalHandlers) {
globalScope.__pathologyComponentModalHandlers = {};
}
function bindDocumentHandler(key, handler) {
var bucket = globalScope.__pathologyComponentModalHandlers;
if (bucket[key]) {
document.removeEventListener('click', bucket[key]);
}
bucket[key] = handler;
document.addEventListener('click', handler);
}
input.addEventListener('input', function () {
var q = String(this.value || '').trim();
selected = null;
clearTimeout(searchTimer);
if (q.length < 2) {
hideSuggestionBox();
return;
}
searchTimer = setTimeout(function () {
fetch(GATEWAY_COMPONENT_SEARCH_URL + '?q=' + encodeURIComponent(q), {
headers: { 'X-Requested-With': 'XMLHttpRequest' }
})
.then(function (r) { return r.json(); })
.then(function (resp) {
if (!resp || Number(resp.ok || 0) !== 1) {
hideSuggestionBox();
return;
}
renderSuggestionBox(Array.isArray(resp.items) ? resp.items : []);
})
.catch(function () {
hideSuggestionBox();
});
}, 260);
});
input.addEventListener('keydown', function (event) {
if (box.style.display !== 'block' || !suggestionItems.length) {
return;
}
if (event.key === 'ArrowDown') {
event.preventDefault();
setActiveSuggestion(activeIndex + 1);
return;
}
if (event.key === 'ArrowUp') {
event.preventDefault();
setActiveSuggestion(activeIndex - 1);
return;
}
if (event.key === 'Enter') {
if (activeIndex >= 0) {
event.preventDefault();
suggestionItems[activeIndex].click();
}
return;
}
if (event.key === 'Escape') {
hideSuggestionBox();
}
});
addBtn.addEventListener('click', function () {
var repoId = <?= (int) ($mstRepoKey ?? 0) ?>;
if (!selected || !selected.name) {
if (typeof notify === 'function') {
notify('warning', 'Select component', 'Search and select a gateway component first.');
}
input.focus();
return;
}
var csrfValue = (window.jQuery && $('input[name="<?= csrf_token() ?>"]').first().val()) || '<?= csrf_hash() ?>';
var fd = new FormData();
fd.append('repo_id', String(repoId));
fd.append('component_name', String(selected.name || ''));
fd.append('short_name', String(selected.short_name || ''));
fd.append('code', String(selected.code || ''));
fd.append('unit', String(selected.unit || ''));
fd.append('property', String(selected.property || ''));
fd.append('specimen_system', String(selected.specimen_system || ''));
fd.append('scale_type', String(selected.scale_type || ''));
fd.append('<?= csrf_token() ?>', csrfValue);
addBtn.disabled = true;
fetch(GATEWAY_COMPONENT_ADD_URL, {
method: 'POST',
headers: { 'X-Requested-With': 'XMLHttpRequest' },
body: fd,
})
.then(function (r) { return r.json(); })
.then(function (resp) {
addBtn.disabled = false;
if (!resp || Number(resp.ok || 0) !== 1) {
if (typeof notify === 'function') {
notify('warning', 'Add failed', (resp && resp.error) ? resp.error : 'Unable to add gateway component.');
}
return;
}
if (typeof notify === 'function') {
notify('success', 'Component attached', String(resp.message || 'Added successfully.'));
}
load_form_div(RELOAD_COMPONENT_LIST_URL + repoId, 'test_div');
})
.catch(function () {
addBtn.disabled = false;
if (typeof notify === 'function') {
notify('warning', 'Add failed', 'Network/server error while adding gateway component.');
}
});
});
bindDocumentHandler('componentSuggestionOutsideClick', function (event) {
if (!input.contains(event.target) && !box.contains(event.target)) {
hideSuggestionBox();
}
});
}());
</script>

View File

@ -0,0 +1,231 @@
<?php
/**
* LOINC Mapping Admin Panel — PathLab_Report/loinc_panel.php
*
* Shows all lab panels (lab_repo) and their component tests (lab_tests)
* with editable LOINC codes. Includes a "Sync from Bridge API" button.
*
* Variables injected by Setting\Template::loincPanel():
* $panels — array of lab_repo rows (mstRepoKey, Title, loinc_code, loinc_synced_at)
* $testsByRepo — [mstRepoKey => [lab_tests rows with loinc_* columns]]
*/
?>
<section class="content">
<div class="container-fluid">
<div class="d-flex align-items-center justify-content-between mb-3 flex-wrap gap-2">
<h4 class="mb-0"><i class="fas fa-flask me-2 text-primary"></i>LOINC Mapping — Pathology Tests</h4>
<div class="d-flex gap-2 flex-wrap">
<button id="btn-sync-loinc" class="btn btn-sm btn-success">
<i class="fas fa-sync-alt me-1"></i>Sync from Bridge API
</button>
<a href="<?= site_url('Lab_Admin/report_list') ?>" class="btn btn-sm btn-secondary">
<i class="fas fa-arrow-left me-1"></i>Back to Report List
</a>
</div>
</div>
<!-- Sync output box -->
<div id="sync-output-box" class="alert alert-info d-none mb-3" style="white-space:pre-wrap;font-family:monospace;font-size:0.82rem;max-height:220px;overflow-y:auto;"></div>
<p class="text-muted small mb-3">
LOINC codes allow lab results to be published as structured FHIR <strong>Observations</strong> inside
ABDM <em>DiagnosticReportRecord</em> bundles. Blank LOINC codes are sent as display-only text observations.
Use <kbd>Sync from Bridge API</kbd> for bulk auto-matching or edit individual rows below.
</p>
<?php if (empty($panels)): ?>
<div class="alert alert-warning">No lab panels found. Add panels via <a href="<?= site_url('Lab_Admin/report_list') ?>">Report List</a>.</div>
<?php else: ?>
<?php foreach ($panels as $panel):
$panelId = (int) $panel['mstRepoKey'];
$panelTitle = esc($panel['Title'] ?? '');
$panelLoinc = esc($panel['loinc_code'] ?? '');
$panelSync = esc($panel['loinc_synced_at'] ?? '');
$tests = $testsByRepo[$panelId] ?? [];
$hasPanelLoinc = trim($panel['loinc_code'] ?? '') !== '';
?>
<div class="card mb-3 shadow-sm">
<div class="card-header py-2 d-flex align-items-center justify-content-between flex-wrap gap-2 bg-light">
<div>
<strong><?= $panelTitle ?></strong>
<?php if ($hasPanelLoinc): ?>
<span class="badge bg-success ms-2" title="LOINC Panel Code"><?= $panelLoinc ?></span>
<?php else: ?>
<span class="badge bg-secondary ms-2">No Panel LOINC</span>
<?php endif; ?>
<?php if ($panelSync): ?>
<small class="text-muted ms-2">synced: <?= $panelSync ?></small>
<?php endif; ?>
</div>
<div class="d-flex gap-2 align-items-center">
<input type="text" class="form-control form-control-sm loinc-panel-input"
style="width:130px"
placeholder="Panel LOINC"
value="<?= $panelLoinc ?>"
data-id="<?= $panelId ?>"
title="LOINC code for panel (DiagnosticReport.code)">
<button class="btn btn-xs btn-outline-primary btn-save-panel"
data-id="<?= $panelId ?>">Save</button>
</div>
</div>
<?php if (! empty($tests)): ?>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-sm table-bordered mb-0 align-middle" style="font-size:0.82rem">
<thead class="table-light">
<tr>
<th style="width:22%">Test Name</th>
<th style="width:8%">Unit</th>
<th style="width:14%">LOINC Code</th>
<th style="width:16%">Property</th>
<th style="width:16%">System</th>
<th style="width:12%">Scale</th>
<th style="width:8%">Synced</th>
<th style="width:4%"></th>
</tr>
</thead>
<tbody>
<?php foreach ($tests as $t):
$tid = (int) $t['mstTestKey'];
$tname = esc($t['Test'] ?? '');
$tunit = esc($t['Unit'] ?? '');
$tloinc = esc($t['loinc_code'] ?? '');
$tprop = esc($t['loinc_property'] ?? '');
$tsys = esc($t['loinc_system'] ?? '');
$tscale = esc($t['loinc_scale'] ?? '');
$tsync = esc($t['loinc_synced_at'] ?? '');
$hasLoinc = trim($t['loinc_code'] ?? '') !== '';
?>
<tr data-test-id="<?= $tid ?>">
<td>
<?= $tname ?>
<?php if ($hasLoinc): ?><i class="fas fa-check-circle text-success ms-1" title="Has LOINC"></i><?php endif; ?>
</td>
<td class="text-muted"><?= $tunit ?></td>
<td><input type="text" class="form-control form-control-sm loinc-test-code"
value="<?= $tloinc ?>" placeholder="e.g. 718-7" style="width:100%"></td>
<td><input type="text" class="form-control form-control-sm loinc-test-prop"
value="<?= $tprop ?>" placeholder="MCnc" style="width:100%"></td>
<td><input type="text" class="form-control form-control-sm loinc-test-sys"
value="<?= $tsys ?>" placeholder="Bld" style="width:100%"></td>
<td><input type="text" class="form-control form-control-sm loinc-test-scale"
value="<?= $tscale ?>" placeholder="Qn" style="width:100%"></td>
<td class="text-muted" style="font-size:0.75rem"><?= $tsync ? date('d-m-Y', strtotime($tsync)) : '-' ?></td>
<td>
<button class="btn btn-xs btn-outline-success btn-save-test" data-test-id="<?= $tid ?>" title="Save LOINC">
<i class="fas fa-save"></i>
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div><!-- /container-fluid -->
</section>
<script>
(function () {
'use strict';
const SYNC_URL = '<?= site_url('Lab_Admin/loinc_sync') ?>';
const UPDATE_URL = '<?= site_url('Lab_Admin/loinc_update') ?>';
// ── Sync button ──────────────────────────────────────────────────────────
document.getElementById('btn-sync-loinc').addEventListener('click', function () {
const btn = this;
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Syncing…';
const outputBox = document.getElementById('sync-output-box');
outputBox.classList.add('d-none');
outputBox.textContent = '';
const fd = new FormData();
fetch(SYNC_URL, { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' }, body: fd })
.then(r => r.json())
.then(data => {
outputBox.classList.remove('d-none');
outputBox.classList.toggle('alert-success', data.ok == 1);
outputBox.classList.toggle('alert-danger', data.ok != 1);
outputBox.classList.remove('alert-info');
outputBox.textContent = data.output || (data.ok == 1 ? 'Sync complete.' : 'Sync failed.');
if (data.ok == 1) {
setTimeout(() => location.reload(), 2000);
}
})
.catch(err => {
outputBox.classList.remove('d-none', 'alert-info');
outputBox.classList.add('alert-danger');
outputBox.textContent = 'Request error: ' + err.message;
})
.finally(() => {
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-sync-alt me-1"></i>Sync from Bridge API';
});
});
// ── Save panel LOINC ─────────────────────────────────────────────────────
document.querySelectorAll('.btn-save-panel').forEach(function (btn) {
btn.addEventListener('click', function () {
const id = this.dataset.id;
const input = document.querySelector('.loinc-panel-input[data-id="' + id + '"]');
const code = input ? input.value.trim() : '';
const fd = new FormData();
fd.append('type', 'panel');
fd.append('id', id);
fd.append('loinc_code', code);
fetch(UPDATE_URL, { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' }, body: fd })
.then(r => r.json())
.then(data => {
input.classList.toggle('is-valid', data.ok == 1);
input.classList.toggle('is-invalid', data.ok != 1);
setTimeout(() => input.classList.remove('is-valid', 'is-invalid'), 2000);
});
});
});
// ── Save individual test LOINC ───────────────────────────────────────────
document.querySelectorAll('.btn-save-test').forEach(function (btn) {
btn.addEventListener('click', function () {
const tid = this.dataset.testId;
const row = document.querySelector('tr[data-test-id="' + tid + '"]');
if (! row) return;
const code = row.querySelector('.loinc-test-code')?.value.trim() ?? '';
const prop = row.querySelector('.loinc-test-prop')?.value.trim() ?? '';
const sys = row.querySelector('.loinc-test-sys')?.value.trim() ?? '';
const scale = row.querySelector('.loinc-test-scale')?.value.trim() ?? '';
const fd = new FormData();
fd.append('type', 'test');
fd.append('id', tid);
fd.append('loinc_code', code);
fd.append('loinc_property', prop);
fd.append('loinc_system', sys);
fd.append('loinc_scale', scale);
fetch(UPDATE_URL, { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' }, body: fd })
.then(r => r.json())
.then(data => {
const icon = btn.querySelector('i');
if (icon) {
icon.className = data.ok == 1 ? 'fas fa-check text-success' : 'fas fa-times text-danger';
setTimeout(() => { icon.className = 'fas fa-save'; }, 2000);
}
});
});
});
}());
</script>

View File

@ -153,7 +153,7 @@ $hasTemplateAccess = $canPathology || $canUltrasound || $canXray || $canCt || $c
<div class="row g-3 admin-tiles">
<?php if ($canPathology) { ?>
<div class="col-6 col-md-2 col-lg-2">
<a class="admin-tile" href="javascript:load_form_div('<?= base_url('Lab_Admin/report_list') ?>','maindiv','Pathology Template');">
<a class="admin-tile" href="javascript:load_form('<?= base_url('Lab_Admin/report_list') ?>','Pathology Template');">
<span class="tile-icon"><i class="bi bi-heart-pulse"></i></span>
<span>Pathology Template</span>
</a>

View File

@ -178,6 +178,11 @@
<input type="text" class="form-control form-control-sm" id="tb_abdm_consent_handle" placeholder="Auto-filled after consent request">
</div>
<div class="mb-2">
<label class="form-label mb-1 small">Gateway Consent ID</label>
<input type="text" class="form-control form-control-sm" id="tb_abdm_gateway_consent_id" placeholder="Auto-filled if returned by gateway" readonly>
</div>
<div class="row g-2 mb-2">
<div class="col-md-7">
<input type="number" min="1" class="form-control form-control-sm" id="tb_abdm_claim_document_id" placeholder="Claim Document ID">
@ -385,6 +390,7 @@
var sandboxPurposeCodeInput = document.getElementById('tb_abdm_purpose_code');
var sandboxConsentExpiresInput = document.getElementById('tb_abdm_consent_expires_at');
var sandboxConsentHandleInput = document.getElementById('tb_abdm_consent_handle');
var sandboxGatewayConsentIdInput = document.getElementById('tb_abdm_gateway_consent_id');
var sandboxClaimDocumentInput = document.getElementById('tb_abdm_claim_document_id');
var quickOpdInput = null;
var quickSessionInput = null;
@ -725,6 +731,7 @@
sandboxQrPayloadInput.value = '';
sandboxPurposeCodeInput.value = sandboxPurposeCodeInput.value || 'CAREMGT';
sandboxConsentHandleInput.value = '';
sandboxGatewayConsentIdInput.value = '';
sandboxClaimDocumentInput.value = '';
clearScanResolvePane();
@ -1028,7 +1035,14 @@
if (res.consent_handle) {
sandboxConsentHandleInput.value = (res.consent_handle || '').toString();
}
setSandboxStatus('Consent requested. Handle: ' + (res.consent_handle || '-'), 'success');
if (res.gateway_consent_id) {
sandboxGatewayConsentIdInput.value = (res.gateway_consent_id || '').toString();
}
var statusText = 'Consent requested. Handle: ' + (res.consent_handle || '-');
if (res.gateway_consent_id) {
statusText += ' | Consent ID: ' + res.gateway_consent_id;
}
setSandboxStatus(statusText, 'success');
});
});

View File

@ -168,6 +168,59 @@
return true;
}
function executeInjectedScripts(containerId) {
var container = document.getElementById(containerId);
if (!container) {
return;
}
// Some pages (dashboard charts) can be reloaded into the same canvas IDs.
// Destroy existing chart instances before executing injected scripts.
if (window.Chart) {
var canvases = container.querySelectorAll('canvas');
canvases.forEach(function(canvasEl) {
try {
if (typeof window.Chart.getChart === 'function') {
var existingChart = window.Chart.getChart(canvasEl);
if (existingChart) {
existingChart.destroy();
}
}
} catch (e) {
console.warn('Chart cleanup failed', e);
}
});
}
var scripts = container.querySelectorAll('script');
if (!scripts || scripts.length === 0) {
return;
}
scripts.forEach(function(oldScript) {
var newScript = document.createElement('script');
for (var i = 0; i < oldScript.attributes.length; i++) {
var attr = oldScript.attributes[i];
newScript.setAttribute(attr.name, attr.value);
}
if (oldScript.src) {
var existing = document.querySelector('script[src="' + oldScript.src + '"]');
if (existing) {
return;
}
} else {
newScript.text = oldScript.textContent || oldScript.innerText || '';
}
document.head.appendChild(newScript);
if (!oldScript.src) {
document.head.removeChild(newScript);
}
});
}
function handleAuthError(jqXHR) {
if (jqXHR.status === 401 || jqXHR.status === 403) {
alert('You have been logged out. Please login again.');
@ -187,21 +240,27 @@
}
}
$.ajax({
url: ourl,
dataType: "html",
async: true,
timeout: REQUEST_TIMEOUT_MS,
beforeSend: function() {
$('#main').html('loading...');
$("#wait").css("display", "block");
}
})
url: ourl,
dataType: "html",
async: true,
timeout: REQUEST_TIMEOUT_MS,
beforeSend: function() {
$('#main').html('loading...');
$("#wait").css("display", "block");
}
})
.done(function(html) {
if (typeof delete_varible === 'function') {
delete_varible();
}
$("#wait").css("display", "none");
$("#main").html(html);
var mainEl = document.getElementById('main');
if (mainEl) {
mainEl.innerHTML = html;
} else {
$("#main").html(html);
}
executeInjectedScripts('main');
if (typeof initfunc === 'function') {
initfunc();
}
@ -239,15 +298,20 @@
window.load_form_div = function(ourl, xdiv, top_title = '') {
if (!requireJquery()) return;
$.ajax({
url: ourl,
dataType: "html",
cache: false,
url: ourl,
dataType: "html",
cache: false,
async: true,
timeout: REQUEST_TIMEOUT_MS
})
timeout: REQUEST_TIMEOUT_MS
})
.done(function(html) {
$("#" + xdiv).html(html);
var targetEl = document.getElementById(xdiv);
if (targetEl) {
targetEl.innerHTML = html;
} else {
$("#" + xdiv).html(html);
}
executeInjectedScripts(xdiv);
if (typeof initfunc === 'function') {
initfunc();
}
@ -340,6 +404,158 @@
};
}
</script>
<script>
(function() {
if (!window.jQuery) {
return;
}
var SEARCH_URL = '<?= base_url('Lab_Admin/pathology_masters_search') ?>';
var debounceTimer = null;
function escHtml(str) {
return String(str || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function getModalEls() {
return {
input: document.getElementById('add_panel_name'),
list: document.getElementById('add-panel-suggestions'),
spinner: document.getElementById('add-panel-search-spinner'),
meta: document.getElementById('add-panel-master-meta')
};
}
function hideList() {
var els = getModalEls();
if (!els.list) {
return;
}
els.list.style.display = 'none';
els.list.innerHTML = '';
}
function renderList(items) {
var els = getModalEls();
if (!els.list) {
return;
}
els.list.innerHTML = '';
if (!items || items.length === 0) {
hideList();
return;
}
items.forEach(function(item) {
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'list-group-item list-group-item-action d-flex justify-content-between align-items-center';
btn.innerHTML = '<span>' + escHtml(item.name || item.test_name || '') + '</span>'
+ (item.loinc_code ? '<span class="badge bg-success ms-2">' + escHtml(item.loinc_code) + '</span>' : '<span class="badge bg-light text-muted border ms-2">No LOINC</span>');
btn.addEventListener('click', function() {
if (els.input) {
els.input.value = String(item.test_name || item.name || '').trim();
}
if (els.meta) {
els.meta.innerHTML = 'Selected from ABDM API: ' + escHtml(item.test_name || item.name || '');
}
hideList();
});
els.list.appendChild(btn);
});
els.list.style.display = 'block';
}
$(document)
.off('input.abdmPanelSearch', '#add_panel_name')
.on('input.abdmPanelSearch', '#add_panel_name', function() {
clearTimeout(debounceTimer);
var q = String(this.value || '').trim();
var els = getModalEls();
if (q.length < 2) {
hideList();
if (els.meta) {
els.meta.innerHTML = 'Type at least 2 letters to live-search panel templates from ABDM Gateway API.';
}
return;
}
if (els.spinner) {
els.spinner.classList.remove('d-none');
}
if (els.meta) {
els.meta.innerHTML = 'Searching panel templates from ABDM Gateway API...';
}
debounceTimer = setTimeout(function() {
$.ajax({
url: SEARCH_URL,
method: 'GET',
dataType: 'json',
data: { q: q, source: 'api', panel_only: 1 },
cache: false,
headers: { 'X-Requested-With': 'XMLHttpRequest' }
})
.done(function(payload) {
if (els.spinner) {
els.spinner.classList.add('d-none');
}
if (payload && !Array.isArray(payload) && Number(payload.ok || 0) === 0) {
hideList();
if (els.meta) {
els.meta.innerHTML = 'Search failed: ' + escHtml(payload.error || 'Gateway API search failed');
}
return;
}
var items = Array.isArray(payload)
? payload
: (payload && Array.isArray(payload.items) ? payload.items : []);
renderList(items);
if (els.meta) {
els.meta.innerHTML = items.length > 0
? ('Found ' + items.length + ' API matches')
: ('No API matches for "' + escHtml(q) + '"');
}
})
.fail(function(jqXHR, textStatus) {
if (els.spinner) {
els.spinner.classList.add('d-none');
}
hideList();
if (els.meta) {
els.meta.innerHTML = 'API search request failed (' + escHtml(textStatus || 'error') + ').';
}
});
}, 280);
});
$(document)
.off('click.abdmPanelSearchDoc')
.on('click.abdmPanelSearchDoc', function(e) {
var input = document.getElementById('add_panel_name');
var list = document.getElementById('add-panel-suggestions');
if (!input || !list) {
return;
}
if (!input.contains(e.target) && !list.contains(e.target)) {
hideList();
}
});
})();
</script>
</body>
</html>