feat(abdm): show health documents in HIP link modal and create dedicated ABHA Filter & FHIR Inspector page
This commit is contained in:
parent
6ac211377b
commit
0fe48f2ec8
|
|
@ -857,6 +857,11 @@ $routes->get('AbdmTaskBoard', 'AbdmTaskBoard::index', ['filter' => $abdmPermFilt
|
|||
$routes->get('AbdmTaskBoard/list', 'AbdmTaskBoard::list', ['filter' => $abdmPermFilter]);
|
||||
$routes->post('AbdmTaskBoard/perform_action', 'AbdmTaskBoard::performAction', ['filter' => $abdmPermFilter]);
|
||||
$routes->post('AbdmTaskBoard/mark_status', 'AbdmTaskBoard::markStatus', ['filter' => $abdmPermFilter]);
|
||||
// ABHA Filter & FHIR Inspector
|
||||
$routes->get('AbdmFhirInspector', 'AbdmFhirInspector::index', ['filter' => $abdmPermFilter]);
|
||||
$routes->get('AbdmFhirInspector/patient_data', 'AbdmFhirInspector::patientData', ['filter' => $abdmPermFilter]);
|
||||
$routes->get('AbdmFhirInspector/gateway_check', 'AbdmFhirInspector::gatewayCheck', ['filter' => $abdmPermFilter]);
|
||||
$routes->get('AbdmFhirInspector/preview_bundle', 'AbdmFhirInspector::previewBundle', ['filter' => $abdmPermFilter]);
|
||||
// ABDM Bridge Log viewer
|
||||
$routes->get('AbdmBridgeLog', 'AbdmBridgeLog::index', ['filter' => 'permission:abdm.access,abdm.taskboard.access,abdm.gateway.use,abdm.bridge_log.view']);
|
||||
$routes->get('AbdmBridgeLog/list', 'AbdmBridgeLog::list', ['filter' => 'permission:abdm.access,abdm.taskboard.access,abdm.gateway.use,abdm.bridge_log.view']);
|
||||
|
|
|
|||
362
app/Controllers/AbdmFhirInspector.php
Normal file
362
app/Controllers/AbdmFhirInspector.php
Normal file
|
|
@ -0,0 +1,362 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Libraries\Abdm\AbdmConnectorFactory;
|
||||
use App\Libraries\Abdm\EAtriaBridgeConnector;
|
||||
|
||||
/**
|
||||
* AbdmFhirInspector
|
||||
*
|
||||
* Dedicated standalone ABDM Page for "ABHA Filter & FHIR Inspector":
|
||||
* - Global ABHA Search & Filter across all patient records and care contexts
|
||||
* - Live ABDM Gateway Verification (Bridge / National Registry)
|
||||
* - Full Patient FHIR R4 Bundle Inspector (OPConsult, DiagnosticReport, HealthDocument, Wellness, Invoice, Immunization, DischargeSummary)
|
||||
* - One-click FHIR Preview with formatted JSON viewer and validation diagnostics
|
||||
* - Direct Care Context Linking to ABHA
|
||||
*/
|
||||
class AbdmFhirInspector extends BaseController
|
||||
{
|
||||
private $db;
|
||||
private ?EAtriaBridgeConnector $connector = null;
|
||||
|
||||
public function initController(
|
||||
\CodeIgniter\HTTP\RequestInterface $request,
|
||||
\CodeIgniter\HTTP\ResponseInterface $response,
|
||||
\Psr\Log\LoggerInterface $logger
|
||||
) {
|
||||
parent::initController($request, $response, $logger);
|
||||
$this->db = \Config\Database::connect();
|
||||
try {
|
||||
$this->connector = AbdmConnectorFactory::bridge();
|
||||
} catch (\Throwable $e) {
|
||||
$this->connector = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main page view for ABHA Filter & FHIR Inspector
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$filterAbha = trim((string) ($this->request->getGet('abha_address') ?? $this->request->getGet('abha') ?? ''));
|
||||
$patientId = (int) ($this->request->getGet('patient_id') ?? $this->request->getGet('id') ?? 0);
|
||||
$uhid = trim((string) ($this->request->getGet('uhid') ?? ''));
|
||||
|
||||
$abhaPatients = $this->getAbhaPatientsList();
|
||||
|
||||
return view('abdm/fhir_inspector', [
|
||||
'filter_abha' => $filterAbha,
|
||||
'patient_id' => $patientId,
|
||||
'filter_uhid' => $uhid,
|
||||
'abha_patients' => $abhaPatients,
|
||||
'recent_pills' => array_slice($abhaPatients, 0, 8),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX endpoint: Get patient demographics, care contexts, and link status
|
||||
* GET /AbdmFhirInspector/patient_data?abha_address=...&patient_id=...
|
||||
*/
|
||||
public function patientData()
|
||||
{
|
||||
$patientId = (int) ($this->request->getGet('patient_id') ?? 0);
|
||||
$abhaAddress = trim((string) ($this->request->getGet('abha_address') ?? $this->request->getGet('abha') ?? ''));
|
||||
$pCode = trim((string) ($this->request->getGet('p_code') ?? $this->request->getGet('uhid') ?? ''));
|
||||
|
||||
if ($patientId <= 0 && $abhaAddress === '' && $pCode === '') {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'ABHA Address, Patient ID, or UHID is required']);
|
||||
}
|
||||
|
||||
// Delegate to AbdmGateway for canonical patient discovery & care contexts
|
||||
try {
|
||||
$gateway = new \App\Controllers\AbdmGateway();
|
||||
$gateway->initController($this->request, $this->response, service('logger'));
|
||||
return $gateway->hipPatientCareContexts();
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', '[AbdmFhirInspector::patientData] {msg}', ['msg' => $e->getMessage()]);
|
||||
return $this->response->setJSON([
|
||||
'ok' => 0,
|
||||
'error_text' => 'Unable to load patient care contexts: ' . $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX endpoint: Live Gateway Link Check for an ABHA address
|
||||
* GET /AbdmFhirInspector/gateway_check?abha_address=...
|
||||
*/
|
||||
public function gatewayCheck()
|
||||
{
|
||||
$abha = trim((string) ($this->request->getGet('abha_address') ?? $this->request->getGet('abha') ?? ''));
|
||||
if ($abha === '') {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'ABHA address is required']);
|
||||
}
|
||||
|
||||
try {
|
||||
if ($this->connector === null) {
|
||||
$this->connector = AbdmConnectorFactory::bridge();
|
||||
}
|
||||
$result = $this->connector->hipGetPatientLinks(['abha_address' => $abha]);
|
||||
$careContexts = [];
|
||||
if (! empty($result['data']) && is_array($result['data'])) {
|
||||
foreach ($result['data'] as $patientRec) {
|
||||
foreach ($patientRec['careContexts'] ?? [] as $cc) {
|
||||
$careContexts[] = [
|
||||
'reference' => trim((string) ($cc['referenceNumber'] ?? $cc['ref'] ?? '')),
|
||||
'display' => trim((string) ($cc['display'] ?? '')),
|
||||
'status' => trim((string) ($cc['status'] ?? 'linked')),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->response->setJSON([
|
||||
'ok' => 1,
|
||||
'abha_address' => $abha,
|
||||
'count' => count($careContexts),
|
||||
'care_contexts' => $careContexts,
|
||||
'raw_bridge' => $result,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->response->setJSON([
|
||||
'ok' => 0,
|
||||
'error_text' => 'Bridge check failed: ' . $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX endpoint: Preview FHIR R4 Bundle for any care context reference
|
||||
* GET /AbdmFhirInspector/preview_bundle?ref=...&patient_id=...&abha=...
|
||||
*/
|
||||
public function previewBundle()
|
||||
{
|
||||
$ref = trim((string) ($this->request->getGet('ref') ?? ''));
|
||||
$patientId = (int) ($this->request->getGet('patient_id') ?? 0);
|
||||
$abha = trim((string) ($this->request->getGet('abha') ?? ''));
|
||||
|
||||
if ($ref === '') {
|
||||
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Care Context Reference is required']);
|
||||
}
|
||||
|
||||
// 1. Check health_records table for stored bundle
|
||||
if ($this->db->tableExists('health_records')) {
|
||||
$hr = $this->db->table('health_records')
|
||||
->where('care_context_reference', $ref)
|
||||
->orderBy('id', 'DESC')
|
||||
->get(1)
|
||||
->getRowArray();
|
||||
|
||||
if (! empty($hr)) {
|
||||
$rawBundle = $hr['fhir_bundle'] ?? $hr['bundle'] ?? null;
|
||||
$decoded = null;
|
||||
if (is_string($rawBundle) && $rawBundle !== '') {
|
||||
$decoded = json_decode($rawBundle, true);
|
||||
} elseif (is_array($rawBundle)) {
|
||||
$decoded = $rawBundle;
|
||||
}
|
||||
|
||||
if (! empty($decoded)) {
|
||||
return $this->response->setJSON([
|
||||
'ok' => 1,
|
||||
'source' => 'health_records_stored',
|
||||
'care_context_reference' => $ref,
|
||||
'care_context_display' => $hr['care_context_display'] ?? $ref,
|
||||
'hi_type' => $hr['hi_type'] ?? 'HealthRecord',
|
||||
'bundle' => $decoded,
|
||||
'fhir_bundle' => $decoded,
|
||||
'resource_type' => $decoded['resourceType'] ?? 'Bundle',
|
||||
'total_entries' => count($decoded['entry'] ?? []),
|
||||
'entry_types' => array_values(array_unique(array_filter(array_map(function ($e) {
|
||||
return $e['resource']['resourceType'] ?? null;
|
||||
}, $decoded['entry'] ?? [])))),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Dynamic generation based on prefix
|
||||
try {
|
||||
// OPD Consult
|
||||
if (preg_match('/^OPD-(\d+)(?:-S(\d+))?/i', $ref, $m)) {
|
||||
$opdId = (int) $m[1];
|
||||
$sessionId = ! empty($m[2]) ? (int) $m[2] : $opdId;
|
||||
$opdController = new \App\Controllers\Opd_prescription();
|
||||
$opdController->initController($this->request, $this->response, service('logger'));
|
||||
return $opdController->fhir_bundle_preview($opdId, $sessionId);
|
||||
}
|
||||
|
||||
// Health Document: DOC-file-{id} or DOC-{id}
|
||||
if (preg_match('/^DOC-(?:file-)?(\d+)/i', $ref, $m)) {
|
||||
$docId = (int) $m[1];
|
||||
$docController = new \App\Controllers\DoctorDocument();
|
||||
$docController->initController($this->request, $this->response, service('logger'));
|
||||
return $docController->health_document_fhir_preview($docId);
|
||||
}
|
||||
|
||||
// Diagnostic Report: LAB-{id} or RAD-{id}
|
||||
if (preg_match('/^(?:LAB|RAD)-(\d+)/i', $ref, $m)) {
|
||||
$labId = (int) $m[1];
|
||||
$gw = new \App\Controllers\AbdmGateway();
|
||||
$gw->initController($this->request, $this->response, service('logger'));
|
||||
// Simulate request param
|
||||
$_GET['request_id'] = $labId;
|
||||
return $gw->diagnosisReportFhirPreview();
|
||||
}
|
||||
|
||||
// Invoices: INVOICE-OPD-{id}, INVOICE-CHG-{id}, INVOICE-IPD-{id}
|
||||
if (preg_match('/^INVOICE-([A-Za-z]+)-(\d+)/i', $ref, $m)) {
|
||||
$typeCode = strtoupper($m[1]);
|
||||
$billId = (int) $m[2];
|
||||
$source = match ($typeCode) {
|
||||
'OPD' => 'opd_invoice',
|
||||
'CHG' => 'charges_invoice',
|
||||
'IPD' => 'ipd_invoice',
|
||||
default => 'opd_invoice'
|
||||
};
|
||||
$gw = new \App\Controllers\AbdmGateway();
|
||||
$gw->initController($this->request, $this->response, service('logger'));
|
||||
$_GET['source'] = $source;
|
||||
$_GET['bill_id'] = $billId;
|
||||
$_GET['patient_id'] = $patientId;
|
||||
return $gw->invoiceFhirPreview();
|
||||
}
|
||||
|
||||
// Immunization: IMM-{id}
|
||||
if (preg_match('/^IMM-(\d+)/i', $ref, $m)) {
|
||||
$immId = (int) $m[1];
|
||||
$gw = new \App\Controllers\AbdmGateway();
|
||||
$gw->initController($this->request, $this->response, service('logger'));
|
||||
$_GET['record_id'] = $immId;
|
||||
$_GET['patient_id'] = $patientId;
|
||||
return $gw->immunizationFhirPreview();
|
||||
}
|
||||
|
||||
// Discharge Summary: DISCHARGE-{id}
|
||||
if (preg_match('/^DISCHARGE-(\d+)/i', $ref, $m)) {
|
||||
$ipdId = (int) $m[1];
|
||||
$gw = new \App\Controllers\AbdmGateway();
|
||||
$gw->initController($this->request, $this->response, service('logger'));
|
||||
$_GET['ipd_id'] = $ipdId;
|
||||
return $gw->ipdDischargeFhirPreview();
|
||||
}
|
||||
|
||||
// Wellness Record: WELLNESS-{id}
|
||||
if (preg_match('/^WELLNESS-(?:(\d+)-W)?(\d+)/i', $ref, $m)) {
|
||||
$wId = ! empty($m[2]) ? (int) $m[2] : (int) $m[1];
|
||||
$pId = ! empty($m[1]) ? (int) $m[1] : $patientId;
|
||||
$docController = new \App\Controllers\DoctorDocument();
|
||||
$docController->initController($this->request, $this->response, service('logger'));
|
||||
return $docController->wellness_record_fhir_preview($pId, $wId);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', '[AbdmFhirInspector::previewBundle] Generation failed: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
return $this->response->setStatusCode(404)->setJSON([
|
||||
'ok' => 0,
|
||||
'error_text' => 'Unable to dynamically generate FHIR bundle for care context: ' . $ref,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query patient_master for patients with ABHA data
|
||||
*/
|
||||
private function getAbhaPatientsList(): array
|
||||
{
|
||||
if (! $this->db->tableExists('patient_master')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$fields = $this->db->getFieldNames('patient_master') ?? [];
|
||||
$hasAbhaAddress = in_array('abha_address', $fields, true);
|
||||
$hasAbhaId = in_array('abha_id', $fields, true);
|
||||
$hasAbhaNo = in_array('abha_no', $fields, true);
|
||||
|
||||
if (! $hasAbhaAddress && ! $hasAbhaId && ! $hasAbhaNo) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$select = ['id', 'p_code', 'p_fname', 'gender', 'dob'];
|
||||
if (in_array('p_lname', $fields, true)) {
|
||||
$select[] = 'p_lname';
|
||||
}
|
||||
if (in_array('mphone1', $fields, true)) {
|
||||
$select[] = 'mphone1';
|
||||
}
|
||||
if ($hasAbhaAddress) {
|
||||
$select[] = 'abha_address';
|
||||
}
|
||||
if ($hasAbhaId) {
|
||||
$select[] = 'abha_id';
|
||||
}
|
||||
if ($hasAbhaNo) {
|
||||
$select[] = 'abha_no';
|
||||
}
|
||||
|
||||
$builder = $this->db->table('patient_master')->select(implode(', ', $select));
|
||||
|
||||
$whereOr = [];
|
||||
if ($hasAbhaAddress) {
|
||||
$whereOr[] = "NULLIF(TRIM(abha_address), '') IS NOT NULL";
|
||||
}
|
||||
if ($hasAbhaId) {
|
||||
$whereOr[] = "NULLIF(TRIM(abha_id), '') IS NOT NULL";
|
||||
}
|
||||
if ($hasAbhaNo) {
|
||||
$whereOr[] = "NULLIF(TRIM(abha_no), '') IS NOT NULL";
|
||||
}
|
||||
|
||||
if ($whereOr !== []) {
|
||||
$builder->where('(' . implode(' OR ', $whereOr) . ')', null, false);
|
||||
}
|
||||
|
||||
$rows = $builder->orderBy('id', 'DESC')->limit(300)->get()->getResultArray();
|
||||
$patients = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$rawAddress = trim((string) ($row['abha_address'] ?? ''));
|
||||
$rawId = trim((string) ($row['abha_id'] ?? $row['abha_no'] ?? ''));
|
||||
|
||||
$addr = '';
|
||||
$num = '';
|
||||
|
||||
if (str_contains($rawAddress, '@')) {
|
||||
$addr = $rawAddress;
|
||||
} elseif (str_contains($rawId, '@')) {
|
||||
$addr = $rawId;
|
||||
}
|
||||
|
||||
$d1 = preg_replace('/\D/', '', $rawId);
|
||||
$d2 = preg_replace('/\D/', '', $rawAddress);
|
||||
if (is_string($d1) && strlen($d1) === 14) {
|
||||
$num = $d1;
|
||||
} elseif (is_string($d2) && strlen($d2) === 14) {
|
||||
$num = $d2;
|
||||
}
|
||||
|
||||
if ($addr === '' && $num === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$name = trim(($row['p_fname'] ?? '') . ' ' . ($row['p_lname'] ?? ''));
|
||||
if ($name === '') {
|
||||
$name = 'Patient #' . $row['id'];
|
||||
}
|
||||
|
||||
$patients[] = [
|
||||
'id' => (int) $row['id'],
|
||||
'p_code' => (string) ($row['p_code'] ?? ('P-' . $row['id'])),
|
||||
'name' => $name,
|
||||
'gender' => (string) ($row['gender'] ?? 'M'),
|
||||
'dob' => (string) ($row['dob'] ?? ''),
|
||||
'phone' => (string) ($row['mphone1'] ?? ''),
|
||||
'abha_address' => $addr,
|
||||
'abha_number' => $num,
|
||||
];
|
||||
}
|
||||
|
||||
return $patients;
|
||||
}
|
||||
}
|
||||
|
|
@ -6450,13 +6450,58 @@ class AbdmGateway extends BaseController
|
|||
]);
|
||||
}
|
||||
} elseif ($taskType === 'health_document_publish') {
|
||||
if ($this->db->tableExists('patient_doc')) {
|
||||
$foundDoc = false;
|
||||
if ($this->db->tableExists('file_upload_data')) {
|
||||
$fileRow = $this->db->table('file_upload_data')->where('id', $taskEntityId)->get(1)->getRowArray();
|
||||
if (! empty($fileRow)) {
|
||||
$vDate = ! empty($fileRow['insert_date']) ? $fileRow['insert_date'] : date('Y-m-d');
|
||||
$dateStr = date('d M Y', strtotime((string) $vDate));
|
||||
$cleanDate = date('Ymd', strtotime((string) $vDate));
|
||||
$docTitle = trim((string) ($fileRow['document_type'] ?? $fileRow['scan_type'] ?? $fileRow['content_description'] ?? ''));
|
||||
if ($docTitle === '' || strcasecmp($docTitle, 'Queued for AI analysis') === 0) {
|
||||
$docTitle = 'Health Document';
|
||||
}
|
||||
$ccRef = 'DOC-file-' . $taskEntityId . '-' . $cleanDate;
|
||||
$aliasRef = 'DOC-' . $taskEntityId . '-' . $cleanDate;
|
||||
|
||||
if ($this->db->tableExists('health_records')) {
|
||||
$existingHr = $this->db->table('health_records')
|
||||
->where('patient_id', $patientId)
|
||||
->groupStart()
|
||||
->where('entity_id', (string) $taskEntityId)
|
||||
->orWhere('care_context_reference LIKE', 'DOC-%' . $taskEntityId . '%')
|
||||
->groupEnd()
|
||||
->orderBy('id', 'DESC')
|
||||
->get(1)
|
||||
->getRowArray();
|
||||
if (! empty($existingHr['care_context_reference'])) {
|
||||
$ccRef = trim((string) $existingHr['care_context_reference']);
|
||||
}
|
||||
}
|
||||
|
||||
$display = 'HealthDocumentRecord - ' . $docTitle . ' (' . $dateStr . ')';
|
||||
$addContext([
|
||||
'careContextId' => $ccRef,
|
||||
'referenceNumber' => $ccRef,
|
||||
'alias_reference' => $aliasRef,
|
||||
'display' => $display,
|
||||
'record_type' => 'HealthDocumentRecord',
|
||||
'patient_id' => $patientId,
|
||||
'is_fhir_ready' => true,
|
||||
'is_primary' => true,
|
||||
]);
|
||||
$foundDoc = true;
|
||||
}
|
||||
}
|
||||
if (! $foundDoc && $this->db->tableExists('patient_doc')) {
|
||||
$pdoc = $this->db->table('patient_doc')->where('id', $taskEntityId)->get(1)->getRowArray();
|
||||
if (! empty($pdoc)) {
|
||||
$docDate = ! empty($pdoc['date_issue']) ? $pdoc['date_issue'] : (! empty($pdoc['created_at']) ? $pdoc['created_at'] : date('Y-m-d'));
|
||||
$dateStr = date('d M Y', strtotime((string) $docDate));
|
||||
$ccRef = 'DOC-' . $taskEntityId . '-' . date('Ymd', strtotime((string) $docDate));
|
||||
$display = 'HealthDocumentRecord - ' . (! empty($pdoc['doc_name']) ? $pdoc['doc_name'] : 'Medical Document') . ' (' . $dateStr . ')';
|
||||
$cleanDate = date('Ymd', strtotime((string) $docDate));
|
||||
$ccRef = 'DOC-' . $taskEntityId . '-' . $cleanDate;
|
||||
$docName = ! empty($pdoc['doc_name']) ? $pdoc['doc_name'] : 'Medical Document';
|
||||
$display = 'HealthDocumentRecord - ' . $docName . ' (' . $dateStr . ')';
|
||||
$addContext([
|
||||
'careContextId' => $ccRef,
|
||||
'referenceNumber' => $ccRef,
|
||||
|
|
@ -7043,6 +7088,79 @@ class AbdmGateway extends BaseController
|
|||
}
|
||||
}
|
||||
|
||||
// 6d. Query patient health documents (file_upload_data & patient_doc)
|
||||
if ($this->db->tableExists('file_upload_data')) {
|
||||
$fileDocs = $this->db->table('file_upload_data')
|
||||
->where('pid', $patientId)
|
||||
->orderBy('id', 'DESC')
|
||||
->limit(20)
|
||||
->get()
|
||||
->getResultArray();
|
||||
foreach ($fileDocs as $fd) {
|
||||
$vDate = ! empty($fd['insert_date']) ? $fd['insert_date'] : date('Y-m-d');
|
||||
$dateStr = date('d M Y', strtotime((string) $vDate));
|
||||
$cleanDate = date('Ymd', strtotime((string) $vDate));
|
||||
$docTitle = trim((string) ($fd['document_type'] ?? $fd['scan_type'] ?? $fd['content_description'] ?? ''));
|
||||
if ($docTitle === '' || strcasecmp($docTitle, 'Queued for AI analysis') === 0) {
|
||||
$docTitle = 'Health Document';
|
||||
}
|
||||
$ccRef = 'DOC-file-' . $fd['id'] . '-' . $cleanDate;
|
||||
$aliasRef = 'DOC-' . $fd['id'] . '-' . $cleanDate;
|
||||
|
||||
if ($this->db->tableExists('health_records')) {
|
||||
$existingHr = $this->db->table('health_records')
|
||||
->where('patient_id', $patientId)
|
||||
->groupStart()
|
||||
->where('entity_id', (string) $fd['id'])
|
||||
->orWhere('care_context_reference LIKE', 'DOC-%' . $fd['id'] . '%')
|
||||
->groupEnd()
|
||||
->orderBy('id', 'DESC')
|
||||
->get(1)
|
||||
->getRowArray();
|
||||
if (! empty($existingHr['care_context_reference'])) {
|
||||
$ccRef = trim((string) $existingHr['care_context_reference']);
|
||||
}
|
||||
}
|
||||
|
||||
$display = 'HealthDocumentRecord - ' . $docTitle . ' (' . $dateStr . ')';
|
||||
$addContext([
|
||||
'careContextId' => $ccRef,
|
||||
'referenceNumber' => $ccRef,
|
||||
'alias_reference' => $aliasRef,
|
||||
'display' => $display,
|
||||
'record_type' => 'HealthDocumentRecord',
|
||||
'patient_id' => $patientId,
|
||||
'is_fhir_ready' => true,
|
||||
'is_primary' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
if ($this->db->tableExists('patient_doc')) {
|
||||
$pDocs = $this->db->table('patient_doc')
|
||||
->where('p_id', $patientId)
|
||||
->orderBy('id', 'DESC')
|
||||
->limit(20)
|
||||
->get()
|
||||
->getResultArray();
|
||||
foreach ($pDocs as $pd) {
|
||||
$docDate = ! empty($pd['date_issue']) ? $pd['date_issue'] : (! empty($pd['created_at']) ? $pd['created_at'] : date('Y-m-d'));
|
||||
$dateStr = date('d M Y', strtotime((string) $docDate));
|
||||
$cleanDate = date('Ymd', strtotime((string) $docDate));
|
||||
$ccRef = 'DOC-' . $pd['id'] . '-' . $cleanDate;
|
||||
$docName = ! empty($pd['doc_name']) ? $pd['doc_name'] : 'Medical Document';
|
||||
$display = 'HealthDocumentRecord - ' . $docName . ' (' . $dateStr . ')';
|
||||
$addContext([
|
||||
'careContextId' => $ccRef,
|
||||
'referenceNumber' => $ccRef,
|
||||
'display' => $display,
|
||||
'record_type' => 'HealthDocumentRecord',
|
||||
'patient_id' => $patientId,
|
||||
'is_fhir_ready' => true,
|
||||
'is_primary' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Fallback: default registration file care context
|
||||
if (empty($careContextsV3)) {
|
||||
$ccRef = 'REG-' . $patientRef;
|
||||
|
|
@ -9547,14 +9665,18 @@ class AbdmGateway extends BaseController
|
|||
$adapter = new \App\Libraries\Abdm\Fhir\Support\GatewayPayloadAdapter();
|
||||
$gatewayPayload = $adapter->toGatewayPayload($output, $source, $hfrId);
|
||||
|
||||
$isFromFile = isset($source['record_id']) && str_starts_with((string)$source['record_id'], 'file-');
|
||||
$reqCcRef = trim((string) ($this->request->getPost('care_context_reference') ?? ''));
|
||||
$resolvedCcRef = $reqCcRef !== '' ? $reqCcRef : (string) ($gatewayPayload['care_context_reference'] ?? ('DOC-' . ($source['record_id'] ?? $targetRecordId) . '-' . date('Ymd', strtotime((string)($source['visit_date'] ?? date('Y-m-d'))))));
|
||||
|
||||
return [
|
||||
'hi_type' => 'HealthDocumentRecord',
|
||||
'entity_type' => 'patient_doc',
|
||||
'entity_type' => $isFromFile ? 'patient_document' : 'patient_doc',
|
||||
'entity_id' => (string) $targetRecordId,
|
||||
'patient_id' => $patientId,
|
||||
'patient_name' => $this->patientDisplayName($patientRow),
|
||||
'visit_date' => (string) ($source['visit_date'] ?? date('Y-m-d')),
|
||||
'care_context_reference' => (string) ($gatewayPayload['care_context_reference'] ?? ('HDOC-' . $patientId . '-' . $targetRecordId)),
|
||||
'care_context_reference' => $resolvedCcRef,
|
||||
'care_context_display' => (string) ($source['document_title'] ?? 'Health Document'),
|
||||
'bundle' => (array) ($gatewayPayload['fhir_bundle'] ?? []),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -2279,11 +2279,16 @@ class DoctorDocument extends BaseController
|
|||
'hospitalization' => false,
|
||||
];
|
||||
|
||||
$cleanVisitDate = date('Ymd', strtotime($visitDate));
|
||||
$ccRef = 'DOC-' . $patientDocId . '-' . $cleanVisitDate;
|
||||
|
||||
return [
|
||||
'record_id' => (string) $patientDocId,
|
||||
'session_id' => (string) $patientDocId,
|
||||
'visit_date' => $visitDate,
|
||||
'completed_at' => $issueIso,
|
||||
'care_context_reference' => $ccRef,
|
||||
'care_context_display' => $docTitle . ' (' . date('d M Y', strtotime($visitDate)) . ')',
|
||||
'document_title' => $docTitle,
|
||||
'document_data_base64' => $docDataBase64,
|
||||
'content_type' => $contentType,
|
||||
|
|
@ -2551,11 +2556,16 @@ class DoctorDocument extends BaseController
|
|||
'hospitalization' => false,
|
||||
];
|
||||
|
||||
$cleanVisitDate = date('Ymd', strtotime($visitDate));
|
||||
$ccRef = 'DOC-file-' . $fileUploadId . '-' . $cleanVisitDate;
|
||||
|
||||
return [
|
||||
'record_id' => 'file-' . $fileUploadId,
|
||||
'session_id' => (string) ($fileRow['opd_id'] ?? $fileUploadId),
|
||||
'visit_date' => $visitDate,
|
||||
'completed_at' => $completedAt,
|
||||
'care_context_reference' => $ccRef,
|
||||
'care_context_display' => $docTitle . ' (' . date('d M Y', strtotime($visitDate)) . ')',
|
||||
'document_title' => $docTitle,
|
||||
'document_data_base64' => $bytes !== '' ? base64_encode($bytes) : '',
|
||||
'content_type' => $mimeType,
|
||||
|
|
|
|||
|
|
@ -20,8 +20,10 @@ class HealthDocumentFhirGenerator extends AbstractModuleFhirGenerator
|
|||
$docTitle = 'Medical Certificate';
|
||||
}
|
||||
|
||||
$careContextReference = 'DOC-' . $recordId . '-' . $visitDate . '-' . date('His');
|
||||
$careContextDisplay = $docTitle . ' (' . $visitDate . ')';
|
||||
$cleanDate = str_replace('-', '', $visitDate);
|
||||
$defaultRef = 'DOC-' . $recordId . '-' . $cleanDate;
|
||||
$careContextReference = (string) ($source['care_context_reference'] ?? $defaultRef);
|
||||
$careContextDisplay = (string) ($source['care_context_display'] ?? ($docTitle . ' (' . $visitDate . ')'));
|
||||
|
||||
$builder = new \App\Libraries\Abdm\Fhir\FhirDocumentBuilder();
|
||||
|
||||
|
|
|
|||
736
app/Views/abdm/fhir_inspector.php
Normal file
736
app/Views/abdm/fhir_inspector.php
Normal file
|
|
@ -0,0 +1,736 @@
|
|||
<style>
|
||||
.insp-hero-badge { width: 44px; height: 44px; display: inline-flex; align-items: center; justify-content: center; border-radius: 10px; }
|
||||
.insp-kpi-card { border-radius: 8px; transition: transform .15s ease, box-shadow .15s ease; }
|
||||
.insp-kpi-card:hover { transform: translateY(-2px); box-shadow: 0 .25rem .75rem rgba(0,0,0,.08); }
|
||||
.quick-pill { transition: all 0.15s ease-in-out; cursor: pointer; }
|
||||
.quick-pill:hover { background-color: #0d6efd !important; color: #fff !important; border-color: #0d6efd !important; }
|
||||
.care-context-row { transition: background-color .15s ease; }
|
||||
.font-mono { font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
|
||||
pre.fhir-json-block {
|
||||
background: #0f172a;
|
||||
color: #e2e8f0;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
max-height: 520px;
|
||||
overflow-y: auto;
|
||||
font-size: 12.5px;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container-fluid py-3" id="abdmFhirInspectorPage">
|
||||
<!-- Top Action Breadcrumb & Title -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<span class="insp-hero-badge bg-primary-subtle text-primary border border-primary-subtle shadow-sm">
|
||||
<i class="bi bi-funnel-fill fs-4"></i>
|
||||
</span>
|
||||
<div>
|
||||
<h4 class="mb-0 fw-bold d-flex align-items-center gap-2">
|
||||
ABHA Filter & FHIR Inspector
|
||||
<span class="badge bg-primary fs-6">ABDM R4</span>
|
||||
</h4>
|
||||
<div class="small text-muted">Inspect patient care contexts, verify live national gateway links, and preview FHIR R4 clinical bundles</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2 flex-wrap">
|
||||
<a href="javascript:load_form('<?= base_url('AbdmTaskBoard') ?>','ABDM Task Board')" class="btn btn-sm btn-outline-primary fw-semibold">
|
||||
<i class="bi bi-kanban me-1"></i> ABDM Task Board
|
||||
</a>
|
||||
<button type="button" class="btn btn-sm btn-outline-success fw-semibold" onclick="openAbdmHipLinkModal()">
|
||||
<i class="bi bi-link-45deg me-1"></i> Link Records to ABHA (HIP)
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-info fw-semibold" onclick="openAbdmHipSmsModal()">
|
||||
<i class="bi bi-chat-dots me-1"></i> Deep Link SMS (sms/notify2)
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" id="btnPageRefresh" title="Refresh current patient inspection">
|
||||
<i class="bi bi-arrow-clockwise"></i> Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search & Autocomplete Filter Card -->
|
||||
<div class="card shadow-sm mb-3 border-0 bg-white" style="border-radius: 10px;">
|
||||
<div class="card-body p-3">
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col-12 col-md">
|
||||
<label class="form-label small fw-semibold text-secondary mb-1">
|
||||
<i class="bi bi-search me-1"></i> Search Patient / ABHA Identifier
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-light"><i class="bi bi-person-bounding-box text-muted"></i></span>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="inspSearchInput"
|
||||
list="inspPatientDatalist"
|
||||
placeholder="Enter ABHA Address (e.g. user@sbx), 14-digit ABHA Number, UHID (e.g. P15353), or Patient Name..."
|
||||
value="<?= esc((string) ($filter_abha ?? '')) ?>"
|
||||
autocomplete="off"
|
||||
>
|
||||
<button type="button" class="btn btn-primary px-3 fw-semibold" id="btnSearchPatient">
|
||||
<i class="bi bi-funnel me-1"></i> Inspect Records
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" id="btnClearSearch" title="Clear input">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<datalist id="inspPatientDatalist">
|
||||
<?php foreach (($abha_patients ?? []) as $ap): ?>
|
||||
<?php
|
||||
$val = ! empty($ap['abha_address']) ? $ap['abha_address'] : $ap['abha_number'];
|
||||
$displayLabel = ($ap['name'] ?? '') . ' (' . ($ap['p_code'] ?? '') . ') - ' . (! empty($ap['abha_address']) ? $ap['abha_address'] : $ap['abha_number']);
|
||||
?>
|
||||
<option value="<?= esc($val) ?>"><?= esc($displayLabel) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</datalist>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Patient Pills -->
|
||||
<?php if (! empty($recent_pills ?? [])): ?>
|
||||
<div class="d-flex align-items-center gap-1 flex-wrap mt-2 pt-2 border-top">
|
||||
<span class="text-muted small fw-semibold me-1" style="font-size: 11.5px;"><i class="bi bi-lightning-charge text-warning me-1"></i>Quick Inspect:</span>
|
||||
<?php foreach ($recent_pills as $rp): ?>
|
||||
<?php
|
||||
$val = ! empty($rp['abha_address']) ? $rp['abha_address'] : $rp['abha_number'];
|
||||
if (empty($val)) continue;
|
||||
?>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-light border py-1 px-2 text-primary quick-pill"
|
||||
data-abha="<?= esc($val) ?>"
|
||||
data-name="<?= esc($rp['name'] ?? '') ?>"
|
||||
style="font-size: 11.5px; border-radius: 6px;"
|
||||
>
|
||||
<i class="bi bi-person me-1"></i><?= esc($rp['name'] ?? $val) ?>
|
||||
</button>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State Prompt (shown before search) -->
|
||||
<div id="inspEmptyPrompt" class="card shadow-sm border-0 text-center p-5 bg-white <?= (! empty($filter_abha) || ! empty($patient_id)) ? 'd-none' : '' ?>" style="border-radius: 10px;">
|
||||
<div class="my-3">
|
||||
<span class="d-inline-flex align-items-center justify-content-center bg-light text-primary rounded-circle p-3 mb-3 border">
|
||||
<i class="bi bi-file-earmark-medical fs-1"></i>
|
||||
</span>
|
||||
<h5 class="fw-bold">Select or Search an ABHA Patient</h5>
|
||||
<p class="text-muted small mx-auto" style="max-width: 520px;">
|
||||
Enter an ABHA Address (e.g. <code>91310013085603@sbx</code>), 14-digit ABHA Number, or Patient UHID above, or click any patient from the <strong>Quick Inspect</strong> bar to view all clinical visits, gateway link status, and inspect FHIR R4 bundles.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active Patient Inspector Container (shown after search/selection) -->
|
||||
<div id="inspActivePatientContainer" class="<?= (empty($filter_abha) && empty($patient_id)) ? 'd-none' : '' ?>">
|
||||
|
||||
<!-- Patient Banner Card -->
|
||||
<div class="card shadow-sm mb-3 border-0 bg-white" style="border-radius: 10px; overflow: hidden;">
|
||||
<div class="card-header bg-primary text-white py-3 px-3 d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||
<div class="d-flex align-items-center gap-2 flex-wrap">
|
||||
<i class="bi bi-shield-check fs-4"></i>
|
||||
<h5 class="mb-0 fw-bold" id="dspPatientName">Patient Demographics</h5>
|
||||
<span class="badge bg-white text-primary fw-semibold" id="dspPatientGender">M</span>
|
||||
<span class="badge bg-white text-primary fw-semibold" id="dspPatientYob">YOB: -</span>
|
||||
<span class="badge bg-white text-primary fw-semibold" id="dspPatientUhid">UHID: -</span>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<button type="button" class="btn btn-sm btn-light py-1 px-3 fw-semibold shadow-sm" id="btnLiveGatewayCheck">
|
||||
<i class="bi bi-cloud-check text-primary me-1"></i> Live Gateway Check
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-light py-1 px-3" id="btnLinkThisPatient">
|
||||
<i class="bi bi-link-45deg me-1"></i> Link Hospital Records
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-light py-1 px-2" id="btnReloadPatient" title="Reload patient records">
|
||||
<i class="bi bi-arrow-clockwise"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-3">
|
||||
<div class="row g-3 align-items-center pb-2 border-bottom">
|
||||
<div class="col-md-3">
|
||||
<div class="text-muted small fw-semibold text-uppercase" style="font-size: 11px;">ABHA Address</div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="fw-bold text-primary font-mono" id="dspAbhaAddress" style="word-break: break-all;">-</span>
|
||||
<button class="btn btn-sm btn-link p-0 text-secondary" id="btnCopyAbha" title="Copy ABHA Address"><i class="bi bi-copy"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="text-muted small fw-semibold text-uppercase" style="font-size: 11px;">14-Digit ABHA Number</div>
|
||||
<div class="fw-semibold font-mono text-dark" id="dspAbhaNumber">-</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="text-muted small fw-semibold text-uppercase" style="font-size: 11px;">Mobile Number</div>
|
||||
<div class="fw-semibold text-dark" id="dspPhone">-</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="text-muted small fw-semibold text-uppercase" style="font-size: 11px;">Gateway Link Status</div>
|
||||
<div id="dspGatewayStatus"><span class="badge bg-secondary">Unchecked</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KPI Metric Tiles -->
|
||||
<div class="row g-2 mt-2">
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="border rounded p-3 bg-light text-center insp-kpi-card">
|
||||
<div class="small text-muted fw-semibold text-uppercase" style="font-size: 11px;">Total Care Contexts</div>
|
||||
<div class="fs-4 fw-bold text-dark" id="kpiTotal">0</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="border rounded p-3 bg-light text-center insp-kpi-card">
|
||||
<div class="small text-muted fw-semibold text-uppercase" style="font-size: 11px;">Linked Contexts</div>
|
||||
<div class="fs-4 fw-bold text-success" id="kpiLinked">0</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="border rounded p-3 bg-light text-center insp-kpi-card">
|
||||
<div class="small text-muted fw-semibold text-uppercase" style="font-size: 11px;">Unlinked Contexts</div>
|
||||
<div class="fs-4 fw-bold text-warning" id="kpiUnlinked">0</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="border rounded p-3 bg-light text-center insp-kpi-card">
|
||||
<div class="small text-muted fw-semibold text-uppercase" style="font-size: 11px;">FHIR Ready Bundles</div>
|
||||
<div class="fs-4 fw-bold text-info" id="kpiFhir">0</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Care Contexts Records Table Card -->
|
||||
<div class="card shadow-sm border-0 bg-white" style="border-radius: 10px;">
|
||||
<div class="card-header bg-white py-3 px-3 d-flex justify-content-between align-items-center flex-wrap gap-2 border-bottom">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="fw-bold text-secondary text-uppercase small"><i class="bi bi-journal-medical me-1"></i> Patient Care Contexts & Clinical Bundles</span>
|
||||
<span class="badge bg-secondary-subtle text-secondary" id="tableFilterCountBadge">0 records</span>
|
||||
</div>
|
||||
<!-- Filter Pills -->
|
||||
<div class="d-flex align-items-center gap-1 flex-wrap" id="contextTypeFilters">
|
||||
<button type="button" class="btn btn-sm btn-primary py-0 px-2 filter-pill active" data-filter="all">All</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary py-0 px-2 filter-pill" data-filter="linked">Linked</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary py-0 px-2 filter-pill" data-filter="unlinked">Unlinked</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary py-0 px-2 filter-pill" data-filter="OPConsultRecord">OPConsult</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary py-0 px-2 filter-pill" data-filter="HealthDocumentRecord">HealthDocument</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary py-0 px-2 filter-pill" data-filter="DiagnosticReportRecord">Diagnostic</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary py-0 px-2 filter-pill" data-filter="WellnessRecord">Wellness</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary py-0 px-2 filter-pill" data-filter="InvoiceRecord">Invoice</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0" id="careContextsTable">
|
||||
<thead class="table-light">
|
||||
<tr class="small text-muted text-uppercase" style="font-size: 11px;">
|
||||
<th style="width: 260px;">Care Context Ref</th>
|
||||
<th>Visit / Record Description</th>
|
||||
<th style="width: 170px;">HI Type</th>
|
||||
<th style="width: 200px;">FHIR Bundle Status</th>
|
||||
<th style="width: 140px;">Context Link Status</th>
|
||||
<th style="width: 130px;" class="text-end">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="careContextsTbody">
|
||||
<tr>
|
||||
<td colspan="6" class="text-center text-muted py-4">
|
||||
<div class="spinner-border spinner-border-sm text-primary me-2" role="status"></div>
|
||||
Loading patient care contexts & FHIR status...
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================================================= -->
|
||||
<!-- FHIR R4 Bundle JSON Inspector Modal -->
|
||||
<!-- ========================================================================= -->
|
||||
<div class="modal fade" id="inspFhirModal" tabindex="-1" aria-labelledby="inspFhirModalTitle" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable">
|
||||
<div class="modal-content shadow-lg border-0">
|
||||
<div class="modal-header bg-dark text-white py-2 px-3">
|
||||
<div class="d-flex align-items-center gap-2 flex-wrap">
|
||||
<i class="bi bi-file-earmark-code fs-5 text-info"></i>
|
||||
<h6 class="modal-title fw-bold" id="inspFhirModalTitle">FHIR R4 Bundle Inspector</h6>
|
||||
<span class="badge bg-primary small" id="inspFhirHiTypeBadge">Record</span>
|
||||
<span class="badge bg-secondary-subtle text-light small font-mono" id="inspFhirRefBadge">-</span>
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body p-3 bg-light">
|
||||
<!-- Summary bar -->
|
||||
<div class="card mb-3 border-0 shadow-sm">
|
||||
<div class="card-body py-2 px-3 d-flex justify-content-between align-items-center flex-wrap gap-2 small">
|
||||
<div class="d-flex align-items-center gap-2 flex-wrap">
|
||||
<span class="text-muted">Resource:</span> <strong class="text-primary" id="inspFhirResType">Bundle</strong>
|
||||
<span class="text-muted">|</span>
|
||||
<span class="text-muted">Entries:</span> <span class="badge bg-info text-dark" id="inspFhirEntryCount">0</span>
|
||||
<span class="text-muted">|</span>
|
||||
<span class="text-muted">Components:</span> <span id="inspFhirEntryTypes" class="small">-</span>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" id="btnCopyFhirJson">
|
||||
<i class="bi bi-clipboard me-1"></i>Copy JSON
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" id="btnDownloadFhirJson">
|
||||
<i class="bi bi-download me-1"></i>Download
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Formatted JSON Block -->
|
||||
<div class="position-relative">
|
||||
<pre class="fhir-json-block" id="inspFhirJsonContent">Loading FHIR R4 Bundle JSON...</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer py-2 px-3 bg-white d-flex justify-content-between">
|
||||
<span class="text-muted small"><i class="bi bi-shield-check text-success me-1"></i>ABDM NRCeS FHIR R4 Standard Profile</span>
|
||||
<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Include the HIP Link Modal Partial -->
|
||||
<?= view('partials/abdm_hip_link_modal') ?>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
var currentPatientData = null;
|
||||
var currentCareContexts = [];
|
||||
var currentFilter = 'all';
|
||||
var activeModalJson = null;
|
||||
|
||||
// Helper: Escaping HTML
|
||||
function hesc(str) {
|
||||
if (str === null || str === undefined) return '';
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
// Load Patient Demographics & Care Contexts
|
||||
window.inspectPatient = function(searchVal) {
|
||||
searchVal = (searchVal || '').trim();
|
||||
if (!searchVal) {
|
||||
alert('Please enter an ABHA Address, ABHA Number, or Patient UHID.');
|
||||
return;
|
||||
}
|
||||
|
||||
var emptyPrompt = document.getElementById('inspEmptyPrompt');
|
||||
var container = document.getElementById('inspActivePatientContainer');
|
||||
var tbody = document.getElementById('careContextsTbody');
|
||||
|
||||
if (emptyPrompt) emptyPrompt.classList.add('d-none');
|
||||
if (container) container.classList.remove('d-none');
|
||||
|
||||
tbody.innerHTML = '<tr><td colspan="6" class="text-center text-muted py-4"><div class="spinner-border spinner-border-sm text-primary me-2" role="status"></div>Loading patient care contexts & FHIR status...</td></tr>';
|
||||
|
||||
var url = '<?= base_url('AbdmFhirInspector/patient_data') ?>?abha_address=' + encodeURIComponent(searchVal);
|
||||
|
||||
fetch(url, {
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(res) {
|
||||
if (!res || !res.ok || !res.patient) {
|
||||
var errTxt = (res && (res.error_text || res.error || res.message)) || 'Patient record not found';
|
||||
tbody.innerHTML = '<tr><td colspan="6" class="text-center text-warning py-4"><i class="bi bi-exclamation-triangle-fill me-2 fs-5"></i>' + hesc(errTxt) + ' for <strong>' + hesc(searchVal) + '</strong>.</td></tr>';
|
||||
document.getElementById('dspPatientName').textContent = 'Patient Not Found';
|
||||
document.getElementById('dspAbhaAddress').textContent = searchVal;
|
||||
document.getElementById('dspAbhaNumber').textContent = '-';
|
||||
document.getElementById('dspPhone').textContent = '-';
|
||||
document.getElementById('dspGatewayStatus').innerHTML = '<span class="badge bg-secondary">Unchecked</span>';
|
||||
document.getElementById('kpiTotal').textContent = '0';
|
||||
document.getElementById('kpiLinked').textContent = '0';
|
||||
document.getElementById('kpiUnlinked').textContent = '0';
|
||||
document.getElementById('kpiFhir').textContent = '0';
|
||||
currentPatientData = null;
|
||||
currentCareContexts = [];
|
||||
return;
|
||||
}
|
||||
|
||||
currentPatientData = res.patient;
|
||||
currentCareContexts = res.care_contexts || [];
|
||||
var linkedRefs = res.linked_refs || [];
|
||||
|
||||
// Populate Demographics
|
||||
var p = res.patient;
|
||||
document.getElementById('dspPatientName').textContent = p.name || 'Patient';
|
||||
document.getElementById('dspPatientGender').textContent = p.gender || 'M';
|
||||
document.getElementById('dspPatientYob').textContent = 'YOB: ' + (p.year_of_birth || '-');
|
||||
document.getElementById('dspPatientUhid').textContent = 'UHID: ' + (p.patient_ref || ('P-' + p.id));
|
||||
document.getElementById('dspAbhaAddress').textContent = p.abha_address || searchVal;
|
||||
document.getElementById('dspAbhaNumber').textContent = p.abha_number || '-';
|
||||
document.getElementById('dspPhone').textContent = p.phone || '-';
|
||||
|
||||
// Gateway Status
|
||||
if (linkedRefs.length > 0) {
|
||||
document.getElementById('dspGatewayStatus').innerHTML = '<span class="badge bg-success shadow-sm"><i class="bi bi-shield-check me-1"></i>Gateway Verified (' + linkedRefs.length + ' links)</span>';
|
||||
} else {
|
||||
var localLinked = currentCareContexts.some(function(c) { return c.is_linked; });
|
||||
if (localLinked) {
|
||||
document.getElementById('dspGatewayStatus').innerHTML = '<span class="badge bg-primary shadow-sm"><i class="bi bi-check2-circle me-1"></i>Local Linked</span>';
|
||||
} else {
|
||||
document.getElementById('dspGatewayStatus').innerHTML = '<span class="badge bg-warning text-dark"><i class="bi bi-exclamation-triangle me-1"></i>Unlinked</span>';
|
||||
}
|
||||
}
|
||||
|
||||
// KPIs
|
||||
var totalCnt = currentCareContexts.length;
|
||||
var linkedCnt = 0;
|
||||
var fhirCnt = 0;
|
||||
currentCareContexts.forEach(function(c) {
|
||||
if (c.is_linked) linkedCnt++;
|
||||
if (c.is_fhir_ready) fhirCnt++;
|
||||
});
|
||||
document.getElementById('kpiTotal').textContent = totalCnt;
|
||||
document.getElementById('kpiLinked').textContent = linkedCnt;
|
||||
document.getElementById('kpiUnlinked').textContent = totalCnt - linkedCnt;
|
||||
document.getElementById('kpiFhir').textContent = fhirCnt;
|
||||
|
||||
// Render Table
|
||||
renderCareContextsTable();
|
||||
})
|
||||
.catch(function(err) {
|
||||
tbody.innerHTML = '<tr><td colspan="6" class="text-center text-danger py-4"><i class="bi bi-exclamation-octagon me-1"></i>Failed to inspect patient: ' + hesc(err.message || 'Network error') + '</td></tr>';
|
||||
});
|
||||
};
|
||||
|
||||
function renderCareContextsTable() {
|
||||
var tbody = document.getElementById('careContextsTbody');
|
||||
var countBadge = document.getElementById('tableFilterCountBadge');
|
||||
|
||||
var filtered = currentCareContexts.filter(function(c) {
|
||||
if (currentFilter === 'all') return true;
|
||||
if (currentFilter === 'linked') return !!c.is_linked;
|
||||
if (currentFilter === 'unlinked') return !c.is_linked;
|
||||
return c.hi_type === currentFilter;
|
||||
});
|
||||
|
||||
if (countBadge) {
|
||||
countBadge.textContent = filtered.length + ' / ' + currentCareContexts.length + ' records';
|
||||
}
|
||||
|
||||
if (filtered.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="6" class="text-center text-muted py-4"><i class="bi bi-info-circle me-1"></i>No care contexts found matching the active filter.</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
var html = '';
|
||||
filtered.forEach(function(c) {
|
||||
var ref = c.ref || '';
|
||||
var isLinked = !!c.is_linked;
|
||||
var isFhir = !!c.is_fhir_ready;
|
||||
var hiType = c.hi_type || 'OPConsultRecord';
|
||||
|
||||
var linkBadge = isLinked
|
||||
? '<span class="badge bg-success shadow-sm"><i class="bi bi-check-circle me-1"></i>LINKED</span>'
|
||||
: '<span class="badge bg-warning text-dark"><i class="bi bi-clock me-1"></i>UNLINKED</span>';
|
||||
|
||||
var fhirBadge = isFhir
|
||||
? '<span class="badge bg-info-subtle text-info-emphasis border border-info-subtle"><i class="bi bi-file-earmark-code me-1"></i>Ready</span>'
|
||||
: '<span class="badge bg-warning-subtle text-dark border border-warning small"><i class="bi bi-clock me-1"></i>Draft</span>';
|
||||
|
||||
var previewBtn = '<button type="button" class="btn btn-sm btn-outline-primary py-0 px-2 ms-2 btn-open-fhir" data-ref="' + hesc(ref) + '" data-hitype="' + hesc(hiType) + '"><i class="bi bi-eye me-1"></i>Preview FHIR</button>';
|
||||
|
||||
var actionBtn = '';
|
||||
if (isLinked) {
|
||||
actionBtn = '<span class="badge bg-success-subtle text-success border border-success-subtle py-1 px-2"><i class="bi bi-check-all me-1"></i>Active</span>';
|
||||
} else if (!isFhir) {
|
||||
actionBtn = '<button type="button" class="btn btn-sm btn-outline-secondary py-0 px-2" disabled title="Clinical findings must be documented before linking to ABDM"><i class="bi bi-clock me-1"></i>Incomplete</button>';
|
||||
} else {
|
||||
actionBtn = '<button type="button" class="btn btn-sm btn-outline-success py-0 px-2 btn-table-link" data-ref="' + hesc(ref) + '" data-type="' + hesc(hiType) + '"><i class="bi bi-link-45deg me-1"></i>Link to ABHA</button>';
|
||||
}
|
||||
|
||||
html += '<tr class="care-context-row">';
|
||||
html += '<td><code class="fw-bold text-primary font-mono" style="font-size: 12.5px;">' + hesc(ref) + '</code></td>';
|
||||
html += '<td><span class="fw-semibold text-dark">' + hesc(c.display || ref) + '</span></td>';
|
||||
html += '<td><span class="badge bg-secondary-subtle text-secondary border">' + hesc(hiType) + '</span></td>';
|
||||
html += '<td>' + fhirBadge + previewBtn + '</td>';
|
||||
html += '<td>' + linkBadge + '</td>';
|
||||
html += '<td class="text-end">' + actionBtn + '</td>';
|
||||
html += '</tr>';
|
||||
});
|
||||
|
||||
tbody.innerHTML = html;
|
||||
}
|
||||
|
||||
// Filter Buttons
|
||||
var filterPills = document.querySelectorAll('#contextTypeFilters .filter-pill');
|
||||
filterPills.forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
filterPills.forEach(function(b) {
|
||||
b.classList.remove('btn-primary', 'active');
|
||||
b.classList.add('btn-outline-secondary');
|
||||
});
|
||||
btn.classList.remove('btn-outline-secondary');
|
||||
btn.classList.add('btn-primary', 'active');
|
||||
currentFilter = btn.dataset.filter || 'all';
|
||||
renderCareContextsTable();
|
||||
});
|
||||
});
|
||||
|
||||
// Search Input & Buttons
|
||||
var searchInput = document.getElementById('inspSearchInput');
|
||||
var btnSearch = document.getElementById('btnSearchPatient');
|
||||
var btnClear = document.getElementById('btnClearSearch');
|
||||
|
||||
if (btnSearch) {
|
||||
btnSearch.addEventListener('click', function() {
|
||||
inspectPatient(searchInput.value);
|
||||
});
|
||||
}
|
||||
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
inspectPatient(searchInput.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (btnClear) {
|
||||
btnClear.addEventListener('click', function() {
|
||||
searchInput.value = '';
|
||||
document.getElementById('inspActivePatientContainer').classList.add('d-none');
|
||||
document.getElementById('inspEmptyPrompt').classList.remove('d-none');
|
||||
});
|
||||
}
|
||||
|
||||
// Quick Inspect Pills
|
||||
document.querySelectorAll('.quick-pill').forEach(function(p) {
|
||||
p.addEventListener('click', function() {
|
||||
var val = p.dataset.abha;
|
||||
if (val) {
|
||||
searchInput.value = val;
|
||||
inspectPatient(val);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Copy ABHA Button
|
||||
var btnCopy = document.getElementById('btnCopyAbha');
|
||||
if (btnCopy) {
|
||||
btnCopy.addEventListener('click', function() {
|
||||
var text = document.getElementById('dspAbhaAddress').textContent.trim();
|
||||
if (text && text !== '-') {
|
||||
navigator.clipboard.writeText(text).then(function() {
|
||||
btnCopy.innerHTML = '<i class="bi bi-check text-success"></i>';
|
||||
setTimeout(function() { btnCopy.innerHTML = '<i class="bi bi-copy"></i>'; }, 2000);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Live Gateway Check
|
||||
var btnGateway = document.getElementById('btnLiveGatewayCheck');
|
||||
if (btnGateway) {
|
||||
btnGateway.addEventListener('click', function() {
|
||||
var abha = document.getElementById('dspAbhaAddress').textContent.trim();
|
||||
if (!abha || abha === '-') {
|
||||
alert('No active ABHA address loaded.');
|
||||
return;
|
||||
}
|
||||
var origHtml = btnGateway.innerHTML;
|
||||
btnGateway.disabled = true;
|
||||
btnGateway.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Checking ABDM...';
|
||||
|
||||
fetch('<?= base_url('AbdmFhirInspector/gateway_check') ?>?abha_address=' + encodeURIComponent(abha), {
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' }
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(res) {
|
||||
btnGateway.disabled = false;
|
||||
btnGateway.innerHTML = origHtml;
|
||||
if (!res.ok) {
|
||||
alert('ABDM Gateway Check: ' + (res.error_text || 'Failed to query gateway'));
|
||||
return;
|
||||
}
|
||||
var count = res.count || 0;
|
||||
var careContexts = res.care_contexts || [];
|
||||
if (count > 0) {
|
||||
var list = careContexts.map(function(c, i) { return (i + 1) + '. ' + c.reference + ' (' + (c.display || 'Record') + ')'; }).join('\n');
|
||||
alert('Confirmed: ABDM National Gateway verified ' + count + ' care context(s) linked to ' + abha + ':\n\n' + list);
|
||||
} else {
|
||||
alert('ABDM National Gateway reports 0 care contexts linked to ' + abha + ' yet.');
|
||||
}
|
||||
// Reload patient data to reflect gateway updates
|
||||
inspectPatient(abha);
|
||||
})
|
||||
.catch(function(e) {
|
||||
btnGateway.disabled = false;
|
||||
btnGateway.innerHTML = origHtml;
|
||||
alert('Gateway check request failed: ' + e.message);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Reload Patient Button
|
||||
var btnReload = document.getElementById('btnReloadPatient');
|
||||
var btnPageRef = document.getElementById('btnPageRefresh');
|
||||
[btnReload, btnPageRef].forEach(function(b) {
|
||||
if (b) {
|
||||
b.addEventListener('click', function() {
|
||||
var abha = document.getElementById('dspAbhaAddress').textContent.trim();
|
||||
if (abha && abha !== '-') {
|
||||
inspectPatient(abha);
|
||||
} else if (searchInput.value.trim()) {
|
||||
inspectPatient(searchInput.value.trim());
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Link Hospital Records Button (Header)
|
||||
var btnLinkHead = document.getElementById('btnLinkThisPatient');
|
||||
if (btnLinkHead) {
|
||||
btnLinkHead.addEventListener('click', function() {
|
||||
if (currentPatientData) {
|
||||
openAbdmHipLinkModal(currentPatientData.id, currentPatientData.abha_address, {
|
||||
name: currentPatientData.name,
|
||||
gender: currentPatientData.gender,
|
||||
yob: currentPatientData.year_of_birth,
|
||||
phone: currentPatientData.phone,
|
||||
onLinked: function() {
|
||||
inspectPatient(currentPatientData.abha_address);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
openAbdmHipLinkModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Link Context Button (Row action)
|
||||
document.addEventListener('click', function(e) {
|
||||
var linkBtn = e.target.closest('.btn-table-link');
|
||||
if (linkBtn && currentPatientData) {
|
||||
var ref = linkBtn.dataset.ref;
|
||||
var type = linkBtn.dataset.type;
|
||||
openAbdmHipLinkModal(currentPatientData.id, currentPatientData.abha_address, {
|
||||
name: currentPatientData.name,
|
||||
gender: currentPatientData.gender,
|
||||
yob: currentPatientData.year_of_birth,
|
||||
phone: currentPatientData.phone,
|
||||
care_context: ref,
|
||||
task_type: type,
|
||||
onLinked: function() {
|
||||
inspectPatient(currentPatientData.abha_address);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Preview FHIR Button (Row action)
|
||||
document.addEventListener('click', function(e) {
|
||||
var fhirBtn = e.target.closest('.btn-open-fhir');
|
||||
if (fhirBtn) {
|
||||
var ref = fhirBtn.dataset.ref;
|
||||
var hiType = fhirBtn.dataset.hitype || 'Record';
|
||||
openFhirBundleModal(ref, hiType);
|
||||
}
|
||||
});
|
||||
|
||||
function openFhirBundleModal(ref, hiType) {
|
||||
var modalEl = document.getElementById('inspFhirModal');
|
||||
if (!modalEl) return;
|
||||
var modal = bootstrap.Modal.getInstance(modalEl) || new bootstrap.Modal(modalEl);
|
||||
|
||||
document.getElementById('inspFhirModalTitle').textContent = 'FHIR R4 Bundle - ' + ref;
|
||||
document.getElementById('inspFhirHiTypeBadge').textContent = hiType;
|
||||
document.getElementById('inspFhirRefBadge').textContent = ref;
|
||||
document.getElementById('inspFhirJsonContent').textContent = 'Loading FHIR R4 Bundle JSON from server...';
|
||||
document.getElementById('inspFhirEntryCount').textContent = '...';
|
||||
document.getElementById('inspFhirEntryTypes').textContent = 'Resolving...';
|
||||
|
||||
modal.show();
|
||||
|
||||
var pId = currentPatientData ? currentPatientData.id : 0;
|
||||
var abha = currentPatientData ? currentPatientData.abha_address : '';
|
||||
var url = '<?= base_url('AbdmFhirInspector/preview_bundle') ?>?ref=' + encodeURIComponent(ref) + '&patient_id=' + encodeURIComponent(pId) + '&abha=' + encodeURIComponent(abha);
|
||||
|
||||
fetch(url, {
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' }
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(res) {
|
||||
var bundle = res.bundle || res.fhir_bundle || (res.data && res.data.bundle) || res;
|
||||
activeModalJson = bundle;
|
||||
|
||||
var jsonFormatted = JSON.stringify(bundle, null, 2);
|
||||
document.getElementById('inspFhirJsonContent').textContent = jsonFormatted;
|
||||
|
||||
var entries = bundle.entry || [];
|
||||
document.getElementById('inspFhirEntryCount').textContent = entries.length;
|
||||
document.getElementById('inspFhirResType').textContent = bundle.resourceType || 'Bundle';
|
||||
|
||||
var types = (bundle.entry || []).map(function(e) {
|
||||
return (e.resource && e.resource.resourceType) ? e.resource.resourceType : '';
|
||||
}).filter(Boolean);
|
||||
var uniqueTypes = Array.from(new Set(types));
|
||||
document.getElementById('inspFhirEntryTypes').innerHTML = uniqueTypes.map(function(t) {
|
||||
return '<span class="badge bg-light text-dark border me-1">' + t + '</span>';
|
||||
}).join('');
|
||||
})
|
||||
.catch(function(err) {
|
||||
document.getElementById('inspFhirJsonContent').textContent = 'Error loading bundle: ' + err.message;
|
||||
});
|
||||
}
|
||||
|
||||
// Copy & Download Modal Buttons
|
||||
var btnCopyJson = document.getElementById('btnCopyFhirJson');
|
||||
if (btnCopyJson) {
|
||||
btnCopyJson.addEventListener('click', function() {
|
||||
var content = document.getElementById('inspFhirJsonContent').textContent;
|
||||
if (content) {
|
||||
navigator.clipboard.writeText(content).then(function() {
|
||||
btnCopyJson.innerHTML = '<i class="bi bi-check text-success me-1"></i>Copied!';
|
||||
setTimeout(function() { btnCopyJson.innerHTML = '<i class="bi bi-clipboard me-1"></i>Copy JSON'; }, 2000);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var btnDownloadJson = document.getElementById('btnDownloadFhirJson');
|
||||
if (btnDownloadJson) {
|
||||
btnDownloadJson.addEventListener('click', function() {
|
||||
var content = document.getElementById('inspFhirJsonContent').textContent;
|
||||
if (content) {
|
||||
var blob = new Blob([content], { type: 'application/json' });
|
||||
var a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = 'fhir-bundle-' + Date.now() + '.json';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-inspect if query parameter present
|
||||
var initSearch = '<?= esc((string) ($filter_abha ?? '')) ?>';
|
||||
if (initSearch) {
|
||||
inspectPatient(initSearch);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
|
@ -25,13 +25,16 @@
|
|||
<div class="small text-muted">ABDM operational dashboard and work queues</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<a href="javascript:load_form('<?= base_url('AbdmFhirInspector') ?>','ABHA Filter & FHIR Inspector')" class="btn btn-sm btn-outline-primary fw-semibold">
|
||||
<i class="bi bi-funnel me-1"></i> ABHA Filter & FHIR Inspector
|
||||
</a>
|
||||
<button type="button" class="btn btn-sm btn-outline-success fw-semibold" onclick="openAbdmHipLinkModal()">
|
||||
<i class="bi bi-link-45deg"></i> Link Records to ABHA (HIP)
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-info fw-semibold" onclick="openAbdmHipSmsModal()">
|
||||
<i class="bi bi-chat-dots"></i> Deep Link SMS (sms/notify2)
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" id="btnRefresh">Refresh</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" id="btnRefresh">Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -57,7 +60,12 @@
|
|||
<i class="bi bi-person-badge fs-6"></i>
|
||||
</span>
|
||||
<div>
|
||||
<strong class="small d-block text-dark">ABHA Filter & FHIR Inspector</strong>
|
||||
<strong class="small d-block text-dark">
|
||||
ABHA Filter & FHIR Inspector
|
||||
<a href="javascript:load_form('<?= base_url('AbdmFhirInspector') ?>','ABHA Filter & FHIR Inspector')" class="btn btn-link btn-sm p-0 ms-1 text-decoration-none" title="Open full dedicated page" style="font-size: 11px;">
|
||||
[Open Full Page <i class="bi bi-box-arrow-up-right"></i>]
|
||||
</a>
|
||||
</strong>
|
||||
<span class="text-muted" style="font-size: 11px;">Filter board & inspect patient care contexts</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -747,6 +747,31 @@
|
|||
alertBox.innerHTML += '<div class="mt-2 pt-2 border-top border-success-subtle text-dark small"><i class="bi bi-file-earmark-medical text-success me-1"></i><strong>FHIR Document Stored on Bridge:</strong> Wellness bundle is active on Bridge for Care Context <code>' + (pRes.care_context_reference || firstRef) + '</code>.<br><i class="bi bi-info-circle text-primary me-1"></i><strong>To view in PHR App:</strong> Open your PHR app (e.g. ABHA App) → tap <em>Linked Facilities</em> → tap <em>"Fetch Records" / "Pull Records"</em>.</div>';
|
||||
}
|
||||
}).catch(function() {});
|
||||
} else if (ctxTaskType === 'health_document_publish') {
|
||||
var pushData = new URLSearchParams();
|
||||
pushData.append('record_id', ctxEntityId);
|
||||
pushData.append('entity_id', ctxEntityId);
|
||||
pushData.append('task_id', (window.currentHipTaskContext && window.currentHipTaskContext.taskId) ? window.currentHipTaskContext.taskId : 0);
|
||||
pushData.append('patient_id', ctxPatientId);
|
||||
pushData.append('abha_id', abhaAddress);
|
||||
pushData.append('push_to_gateway', '1');
|
||||
if (firstRef) pushData.append('care_context_reference', firstRef);
|
||||
pushData.append(csrf.name || 'csrf_hms', csrf.hash);
|
||||
|
||||
fetch('<?= base_url('AbdmGateway/share_health_document_bundle') ?>', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: pushData.toString()
|
||||
}).then(function(r) { return r.json(); }).then(function(pRes) {
|
||||
updateCsrf(pRes);
|
||||
if (pRes && pRes.ok === 1 && alertBox) {
|
||||
alertBox.innerHTML += '<div class="mt-2 pt-2 border-top border-success-subtle text-dark small"><i class="bi bi-file-earmark-medical text-success me-1"></i><strong>FHIR Document Stored on Bridge:</strong> Health Document bundle is active on Bridge for Care Context <code>' + (pRes.care_context_reference || firstRef) + '</code>.<br><i class="bi bi-info-circle text-primary me-1"></i><strong>To view in PHR App:</strong> Open your PHR app (e.g. ABHA App) → tap <em>Linked Facilities</em> → tap <em>"Fetch Records" / "Pull Records"</em>.</div>';
|
||||
}
|
||||
}).catch(function() {});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -460,6 +460,12 @@ if (! $canChargesSettings && $user && method_exists($user, 'inGroup')) {
|
|||
<span>ABDM Task Board</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link collapsed" href="javascript:load_form('<?= base_url('AbdmFhirInspector') ?>','ABHA Filter & FHIR Inspector')">
|
||||
<i class="bi bi-funnel"></i>
|
||||
<span>ABHA Filter & FHIR Inspector</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link collapsed" href="javascript:load_form('<?= base_url('AbdmHiu') ?>','ABHA Patient Request List')">
|
||||
<i class="bi bi-clipboard2-pulse"></i>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user