feat(abdm): enhance care context discovery, async token retry, and modal backdrop cleanup
- Add multi-type care context discovery in AbdmGateway (DiagnosticReport, DischargeSummary, Immunization, OPConsult) with target task prioritization - Implement automated retry backoff in EAtriaBridgeConnector for asynchronous ABDM link token callback delivery - Handle already-linked contexts gracefully and synchronize Task Board row status on link completion - Resolve modal backdrop freeze and inactive main screen on radiology report template update in PathLab_Report and welcome_message - Add unit tests CareContextDiscoveryTest and EAtriaBridgeConnectorLinkTest
This commit is contained in:
parent
61206784f3
commit
41d89a1969
|
|
@ -5990,18 +5990,126 @@ class AbdmGateway extends BaseController
|
|||
return count($common) / max(1, count($union));
|
||||
}
|
||||
|
||||
private function findCareContextsForPatient(int $patientId, string $patientRef, string $patientName): array
|
||||
{
|
||||
private function findCareContextsForPatient(
|
||||
int $patientId,
|
||||
string $patientRef,
|
||||
string $patientName,
|
||||
string $taskType = '',
|
||||
string $entityId = '',
|
||||
int $taskId = 0
|
||||
): array {
|
||||
$this->db = $this->db ?? \Config\Database::connect();
|
||||
$careContextsV3 = [];
|
||||
$careContextsFull = [];
|
||||
$seenRefs = [];
|
||||
|
||||
// 1. Query health_records table
|
||||
$addContext = function (array $item) use (&$careContextsV3, &$careContextsFull, &$seenRefs) {
|
||||
$ref = trim((string) ($item['referenceNumber'] ?? $item['careContextId'] ?? ''));
|
||||
if ($ref === '' || isset($seenRefs[$ref])) {
|
||||
return;
|
||||
}
|
||||
$seenRefs[$ref] = true;
|
||||
$careContextsV3[] = [
|
||||
'referenceNumber' => $ref,
|
||||
'display' => $item['display'] ?? $ref,
|
||||
];
|
||||
$careContextsFull[] = $item;
|
||||
};
|
||||
|
||||
// 1. If explicit task requested (e.g. from Task Board row)
|
||||
if ($taskType !== '' && $entityId !== '') {
|
||||
$taskEntityId = (int) $entityId;
|
||||
if (in_array($taskType, ['radiology_report_publish', 'lab_report_publish'], true) && $this->db->tableExists('lab_request')) {
|
||||
$labReq = $this->db->table('lab_request lr')
|
||||
->select('lr.id, lr.patient_id, lr.lab_type, lr.charge_id, lr.report_name, lr.Report_Data, lr.report_data_Impression, lr.status, lr.Request_Date, lr.reported_time, lr.collected_time')
|
||||
->where('lr.id', $taskEntityId)
|
||||
->get(1)
|
||||
->getRowArray();
|
||||
|
||||
if (! empty($labReq)) {
|
||||
$isImaging = ((int) ($labReq['lab_type'] ?? 0) === 6) || str_contains($taskType, 'radiology');
|
||||
$prefix = $isImaging ? 'RAD' : 'LAB';
|
||||
$title = trim((string) ($labReq['report_name'] ?? ''));
|
||||
if ($title === '' && ! empty($labReq['charge_id']) && $this->db->tableExists('radiology_ultrasound_template')) {
|
||||
$tpl = $this->db->table('radiology_ultrasound_template')->select('template_name')->where('id', (int) $labReq['charge_id'])->get(1)->getRowArray();
|
||||
if (! empty($tpl['template_name'])) {
|
||||
$title = trim((string) $tpl['template_name']);
|
||||
}
|
||||
}
|
||||
if ($title === '') {
|
||||
$title = $this->mapLabTypeToTitle((int) ($labReq['lab_type'] ?? 0)) ?: ($isImaging ? 'Radiology Report' : 'Lab Report');
|
||||
}
|
||||
$reportedDate = ! empty($labReq['reported_time'])
|
||||
? $labReq['reported_time']
|
||||
: (! empty($labReq['Request_Date']) ? $labReq['Request_Date'] : date('Y-m-d'));
|
||||
$visitDate = date('Y-m-d', strtotime((string) $reportedDate));
|
||||
$dateStr = date('d M Y', strtotime($visitDate));
|
||||
$ccRef = $prefix . '-' . $taskEntityId . '-' . str_replace('-', '', $visitDate);
|
||||
$display = ($isImaging ? 'Radiology Report - ' : 'Diagnostic Report - ') . $title . ' (' . $dateStr . ')';
|
||||
|
||||
$hasFindings = trim((string) ($labReq['Report_Data'] ?? '')) !== '' || trim((string) ($labReq['report_data_Impression'] ?? '')) !== '';
|
||||
$statusVal = (int) ($labReq['status'] ?? 0);
|
||||
$isReady = in_array($statusVal, [1, 2], true) || $hasFindings;
|
||||
|
||||
$addContext([
|
||||
'careContextId' => $ccRef,
|
||||
'referenceNumber' => $ccRef,
|
||||
'display' => $display,
|
||||
'record_type' => 'DiagnosticReportRecord',
|
||||
'patient_id' => $patientId,
|
||||
'is_fhir_ready' => $isReady,
|
||||
'is_primary' => true,
|
||||
]);
|
||||
}
|
||||
} elseif ($taskType === 'opd_prescription_publish' && $this->db->tableExists('opd_prescription')) {
|
||||
$pr = $this->db->table('opd_prescription')
|
||||
->where('id', $taskEntityId)
|
||||
->orWhere('session_id', $taskEntityId)
|
||||
->get(1)
|
||||
->getRowArray();
|
||||
if (! empty($pr)) {
|
||||
$visitDate = ! empty($pr['date_opd_visit']) ? $pr['date_opd_visit'] : date('Y-m-d', strtotime((string) ($pr['p_datetime'] ?? 'now')));
|
||||
$dateStr = date('d M Y', strtotime((string) $visitDate));
|
||||
$sessionId = ! empty($pr['session_id']) ? $pr['session_id'] : $pr['id'];
|
||||
$ccRef = 'OPD-' . $patientId . '-S' . $sessionId . '-' . str_replace('-', '', (string) $visitDate);
|
||||
$display = 'OPConsultRecord - ' . $dateStr;
|
||||
$addContext([
|
||||
'careContextId' => $ccRef,
|
||||
'referenceNumber' => $ccRef,
|
||||
'display' => $display,
|
||||
'record_type' => 'OPConsultRecord',
|
||||
'patient_id' => $patientId,
|
||||
'is_fhir_ready' => true,
|
||||
'is_primary' => true,
|
||||
]);
|
||||
}
|
||||
} elseif ($taskType === 'ipd_discharge_publish' && $this->db->tableExists('ipd_master')) {
|
||||
$ipd = $this->db->table('ipd_master')->where('id', $taskEntityId)->get(1)->getRowArray();
|
||||
if (! empty($ipd)) {
|
||||
$disDate = ! empty($ipd['discharge_date']) ? $ipd['discharge_date'] : date('Y-m-d');
|
||||
$dateStr = date('d M Y', strtotime((string) $disDate));
|
||||
$ccRef = 'DISCHARGE-' . $taskEntityId . '-' . str_replace('-', '', (string) $disDate);
|
||||
$display = 'DischargeSummaryRecord - ' . $dateStr;
|
||||
$addContext([
|
||||
'careContextId' => $ccRef,
|
||||
'referenceNumber' => $ccRef,
|
||||
'display' => $display,
|
||||
'record_type' => 'DischargeSummaryRecord',
|
||||
'patient_id' => $patientId,
|
||||
'is_fhir_ready' => true,
|
||||
'is_primary' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Query health_records table (persisted FHIR bundles)
|
||||
if ($this->db->tableExists('health_records')) {
|
||||
$rows = $this->db->table('health_records')
|
||||
->select('id, patient_id, abha_id, hi_type, care_context_reference, created_at, updated_at')
|
||||
->where('patient_id', $patientId)
|
||||
->orderBy('id', 'DESC')
|
||||
->limit(200)
|
||||
->limit(100)
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
|
|
@ -6011,71 +6119,198 @@ class AbdmGateway extends BaseController
|
|||
$ccRef = 'HR-' . (int) ($row['id'] ?? 0);
|
||||
}
|
||||
$hiType = trim((string) ($row['hi_type'] ?? 'HealthDocumentRecord'));
|
||||
$dateStr = date('d M Y', strtotime($row['created_at'] ?? $row['updated_at'] ?? 'now'));
|
||||
$dateStr = date('d M Y', strtotime((string) ($row['created_at'] ?? $row['updated_at'] ?? 'now')));
|
||||
$display = $hiType . ' - ' . $dateStr;
|
||||
|
||||
$careContextsV3[] = [
|
||||
$addContext([
|
||||
'careContextId' => $ccRef,
|
||||
'referenceNumber' => $ccRef,
|
||||
'display' => $display,
|
||||
];
|
||||
$careContextsFull[] = [
|
||||
'careContextId' => $ccRef,
|
||||
'referenceNumber' => $ccRef,
|
||||
'display' => $display,
|
||||
'record_type' => $hiType,
|
||||
'patient_id' => $patientId,
|
||||
];
|
||||
'display' => $display,
|
||||
'record_type' => $hiType,
|
||||
'patient_id' => $patientId,
|
||||
'is_fhir_ready' => true,
|
||||
'is_primary' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fallback: query opd_prescription if health_records has no entries
|
||||
if (empty($careContextsV3) && $this->db->tableExists('opd_prescription')) {
|
||||
// 3. Query lab_request table (All Radiology and Lab Reports for this patient)
|
||||
if ($this->db->tableExists('lab_request')) {
|
||||
$labRows = $this->db->table('lab_request lr')
|
||||
->select('lr.id, lr.lab_type, lr.charge_id, lr.report_name, lr.Report_Data, lr.report_data_Impression, lr.status, lr.Request_Date, lr.reported_time, lr.collected_time')
|
||||
->where('lr.patient_id', $patientId)
|
||||
->orderBy('lr.id', 'DESC')
|
||||
->limit(30)
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
foreach ($labRows as $lr) {
|
||||
$isImaging = ((int) ($lr['lab_type'] ?? 0) === 6);
|
||||
$prefix = $isImaging ? 'RAD' : 'LAB';
|
||||
$title = trim((string) ($lr['report_name'] ?? ''));
|
||||
if ($title === '' && ! empty($lr['charge_id']) && $this->db->tableExists('radiology_ultrasound_template')) {
|
||||
$tpl = $this->db->table('radiology_ultrasound_template')->select('template_name')->where('id', (int) $lr['charge_id'])->get(1)->getRowArray();
|
||||
if (! empty($tpl['template_name'])) {
|
||||
$title = trim((string) $tpl['template_name']);
|
||||
}
|
||||
}
|
||||
if ($title === '') {
|
||||
$title = $this->mapLabTypeToTitle((int) ($lr['lab_type'] ?? 0)) ?: ($isImaging ? 'Radiology Report' : 'Lab Report');
|
||||
}
|
||||
$reportedDate = ! empty($lr['reported_time'])
|
||||
? $lr['reported_time']
|
||||
: (! empty($lr['Request_Date']) ? $lr['Request_Date'] : date('Y-m-d'));
|
||||
$visitDate = date('Y-m-d', strtotime((string) $reportedDate));
|
||||
$dateStr = date('d M Y', strtotime($visitDate));
|
||||
$ccRef = $prefix . '-' . $lr['id'] . '-' . str_replace('-', '', $visitDate);
|
||||
$display = ($isImaging ? 'Radiology Report - ' : 'Diagnostic Report - ') . $title . ' (' . $dateStr . ')';
|
||||
|
||||
$hasFindings = trim((string) ($lr['Report_Data'] ?? '')) !== '' || trim((string) ($lr['report_data_Impression'] ?? '')) !== '';
|
||||
$statusVal = (int) ($lr['status'] ?? 0);
|
||||
$isReady = in_array($statusVal, [1, 2], true) || $hasFindings;
|
||||
|
||||
$addContext([
|
||||
'careContextId' => $ccRef,
|
||||
'referenceNumber' => $ccRef,
|
||||
'display' => $display,
|
||||
'record_type' => 'DiagnosticReportRecord',
|
||||
'patient_id' => $patientId,
|
||||
'is_fhir_ready' => $isReady,
|
||||
'is_primary' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Query opd_prescription (OPD consultations & prescriptions)
|
||||
if ($this->db->tableExists('opd_prescription')) {
|
||||
$prescRows = $this->db->table('opd_prescription')
|
||||
->select('id, p_id, date_opd_visit, session_id, p_datetime')
|
||||
->where('p_id', $patientId)
|
||||
->orderBy('id', 'DESC')
|
||||
->limit(50)
|
||||
->limit(30)
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
foreach ($prescRows as $pr) {
|
||||
$visitDate = ! empty($pr['date_opd_visit'])
|
||||
? $pr['date_opd_visit']
|
||||
: date('Y-m-d', strtotime($pr['p_datetime'] ?? 'now'));
|
||||
: date('Y-m-d', strtotime((string) ($pr['p_datetime'] ?? 'now')));
|
||||
$dateStr = date('d M Y', strtotime($visitDate));
|
||||
$sessionId = ! empty($pr['session_id']) ? $pr['session_id'] : $pr['id'];
|
||||
$ccRef = 'OPD-' . $patientId . '-S' . $sessionId . '-' . str_replace('-', '', $visitDate);
|
||||
$display = 'OPConsultRecord - ' . $dateStr;
|
||||
|
||||
$careContextsV3[] = [
|
||||
$addContext([
|
||||
'careContextId' => $ccRef,
|
||||
'referenceNumber' => $ccRef,
|
||||
'display' => $display,
|
||||
];
|
||||
$careContextsFull[] = [
|
||||
'careContextId' => $ccRef,
|
||||
'referenceNumber' => $ccRef,
|
||||
'display' => $display,
|
||||
'record_type' => 'OPConsultRecord',
|
||||
'patient_id' => $patientId,
|
||||
];
|
||||
'display' => $display,
|
||||
'record_type' => 'OPConsultRecord',
|
||||
'patient_id' => $patientId,
|
||||
'is_fhir_ready' => true,
|
||||
'is_primary' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Fallback: default registration file care context
|
||||
// 5. Query ipd_master (Discharge summaries)
|
||||
if ($this->db->tableExists('ipd_master')) {
|
||||
$ipdRows = $this->db->table('ipd_master')
|
||||
->select('id, p_id, discharge_date, register_date, ipd_status')
|
||||
->where('p_id', $patientId)
|
||||
->where('discharge_date IS NOT NULL')
|
||||
->where("discharge_date != '0000-00-00'")
|
||||
->where("discharge_date > '1970-01-01'")
|
||||
->orderBy('id', 'DESC')
|
||||
->limit(10)
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
foreach ($ipdRows as $ipd) {
|
||||
$disDate = date('Y-m-d', strtotime((string) $ipd['discharge_date']));
|
||||
$dateStr = date('d M Y', strtotime($disDate));
|
||||
$ccRef = 'DISCHARGE-' . $ipd['id'] . '-' . str_replace('-', '', $disDate);
|
||||
$display = 'DischargeSummaryRecord - ' . $dateStr;
|
||||
|
||||
$addContext([
|
||||
'careContextId' => $ccRef,
|
||||
'referenceNumber' => $ccRef,
|
||||
'display' => $display,
|
||||
'record_type' => 'DischargeSummaryRecord',
|
||||
'patient_id' => $patientId,
|
||||
'is_fhir_ready' => true,
|
||||
'is_primary' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Query immunization_records / patient_immunization if exists
|
||||
if ($this->db->tableExists('immunization_records')) {
|
||||
$immRows = $this->db->table('immunization_records')
|
||||
->select('id, patient_id, vaccine_name, given_date, abdm_care_context_reference')
|
||||
->where('patient_id', $patientId)
|
||||
->orderBy('id', 'DESC')
|
||||
->limit(10)
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
foreach ($immRows as $imm) {
|
||||
$admDate = ! empty($imm['given_date']) ? date('Y-m-d', strtotime((string) $imm['given_date'])) : date('Y-m-d');
|
||||
$dateStr = date('d M Y', strtotime($admDate));
|
||||
$ccRef = ! empty($imm['abdm_care_context_reference'])
|
||||
? trim((string) $imm['abdm_care_context_reference'])
|
||||
: ('IMM-' . $imm['id'] . '-' . str_replace('-', '', $admDate));
|
||||
$display = 'ImmunizationRecord - ' . ($imm['vaccine_name'] ?? 'Vaccine') . ' (' . $dateStr . ')';
|
||||
|
||||
$addContext([
|
||||
'careContextId' => $ccRef,
|
||||
'referenceNumber' => $ccRef,
|
||||
'display' => $display,
|
||||
'record_type' => 'ImmunizationRecord',
|
||||
'patient_id' => $patientId,
|
||||
'is_fhir_ready' => true,
|
||||
'is_primary' => false,
|
||||
]);
|
||||
}
|
||||
} elseif ($this->db->tableExists('patient_immunization')) {
|
||||
$immRows = $this->db->table('patient_immunization')
|
||||
->select('id, patient_id, vaccine_name, administration_date')
|
||||
->where('patient_id', $patientId)
|
||||
->orderBy('id', 'DESC')
|
||||
->limit(10)
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
foreach ($immRows as $imm) {
|
||||
$admDate = ! empty($imm['administration_date']) ? date('Y-m-d', strtotime((string) $imm['administration_date'])) : date('Y-m-d');
|
||||
$dateStr = date('d M Y', strtotime($admDate));
|
||||
$ccRef = 'IMM-' . $imm['id'] . '-' . str_replace('-', '', $admDate);
|
||||
$display = 'ImmunizationRecord - ' . ($imm['vaccine_name'] ?? 'Vaccine') . ' (' . $dateStr . ')';
|
||||
|
||||
$addContext([
|
||||
'careContextId' => $ccRef,
|
||||
'referenceNumber' => $ccRef,
|
||||
'display' => $display,
|
||||
'record_type' => 'ImmunizationRecord',
|
||||
'patient_id' => $patientId,
|
||||
'is_fhir_ready' => true,
|
||||
'is_primary' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Fallback: default registration file care context
|
||||
if (empty($careContextsV3)) {
|
||||
$ccRef = 'REG-' . $patientRef;
|
||||
$display = 'Patient File - ' . $patientName;
|
||||
$careContextsV3[] = [
|
||||
$addContext([
|
||||
'careContextId' => $ccRef,
|
||||
'referenceNumber' => $ccRef,
|
||||
'display' => $display,
|
||||
];
|
||||
$careContextsFull[] = [
|
||||
'careContextId' => $ccRef,
|
||||
'referenceNumber' => $ccRef,
|
||||
'display' => $display,
|
||||
'record_type' => 'PatientFile',
|
||||
'patient_id' => $patientId,
|
||||
];
|
||||
'display' => $display,
|
||||
'record_type' => 'PatientFile',
|
||||
'patient_id' => $patientId,
|
||||
'is_fhir_ready' => true,
|
||||
'is_primary' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
return [$careContextsV3, $careContextsFull];
|
||||
|
|
@ -10629,10 +10864,16 @@ class AbdmGateway extends BaseController
|
|||
$patientId = 0;
|
||||
$abhaAddress = '';
|
||||
$pCode = '';
|
||||
$taskType = '';
|
||||
$entityId = '';
|
||||
$taskId = 0;
|
||||
if (method_exists($this->request, 'getVar')) {
|
||||
$patientId = (int) ($this->request->getVar('patient_id') ?? 0);
|
||||
$abhaAddress = trim((string) ($this->request->getVar('abha_address') ?? ''));
|
||||
$pCode = trim((string) ($this->request->getVar('p_code') ?? ''));
|
||||
$taskType = trim((string) ($this->request->getVar('task_type') ?? ''));
|
||||
$entityId = trim((string) ($this->request->getVar('entity_id') ?? ''));
|
||||
$taskId = (int) ($this->request->getVar('task_id') ?? 0);
|
||||
}
|
||||
if ($patientId <= 0 && ! empty($_REQUEST['patient_id'])) {
|
||||
$patientId = (int) $_REQUEST['patient_id'];
|
||||
|
|
@ -10643,6 +10884,15 @@ class AbdmGateway extends BaseController
|
|||
if ($pCode === '' && ! empty($_REQUEST['p_code'])) {
|
||||
$pCode = trim((string) $_REQUEST['p_code']);
|
||||
}
|
||||
if ($taskType === '' && ! empty($_REQUEST['task_type'])) {
|
||||
$taskType = trim((string) $_REQUEST['task_type']);
|
||||
}
|
||||
if ($entityId === '' && ! empty($_REQUEST['entity_id'])) {
|
||||
$entityId = trim((string) $_REQUEST['entity_id']);
|
||||
}
|
||||
if ($taskId <= 0 && ! empty($_REQUEST['task_id'])) {
|
||||
$taskId = (int) $_REQUEST['task_id'];
|
||||
}
|
||||
|
||||
if ($patientId <= 0 && $abhaAddress === '' && $pCode === '') {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'patient_id, abha_address, or p_code is required']);
|
||||
|
|
@ -10695,7 +10945,7 @@ class AbdmGateway extends BaseController
|
|||
$rawPhone = trim((string) ($patient['mphone1'] ?? $patient['mphone2'] ?? ''));
|
||||
$cleanPhone = substr(preg_replace('/\D/', '', $rawPhone), -10);
|
||||
|
||||
[$careContextsV3, $careContextsFull] = $this->findCareContextsForPatient($patientId, $patientRef, $patientName);
|
||||
[$careContextsV3, $careContextsFull] = $this->findCareContextsForPatient($patientId, $patientRef, $patientName, $taskType, $entityId, $taskId);
|
||||
|
||||
// Fetch already-linked care contexts from Bridge if ABHA address is present
|
||||
$linkedRefs = [];
|
||||
|
|
@ -10723,10 +10973,12 @@ class AbdmGateway extends BaseController
|
|||
$ref = $cc['referenceNumber'] ?? $cc['careContextId'] ?? '';
|
||||
$isLinked = in_array($ref, $linkedRefs, true);
|
||||
$contextsList[] = [
|
||||
'ref' => $ref,
|
||||
'display' => $cc['display'] ?? $ref,
|
||||
'hi_type' => $cc['record_type'] ?? 'OPConsultRecord',
|
||||
'is_linked' => $isLinked,
|
||||
'ref' => $ref,
|
||||
'display' => $cc['display'] ?? $ref,
|
||||
'hi_type' => $cc['record_type'] ?? 'OPConsultRecord',
|
||||
'is_linked' => $isLinked,
|
||||
'is_fhir_ready' => (bool) ($cc['is_fhir_ready'] ?? true),
|
||||
'is_primary' => (bool) ($cc['is_primary'] ?? false),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -146,6 +146,16 @@ class AbdmTaskBoard extends BaseController
|
|||
}
|
||||
|
||||
foreach ($tasks as &$task) {
|
||||
$taskStatus = strtolower(trim((string) ($task['status'] ?? 'pending')));
|
||||
if (in_array($taskStatus, ['completed', 'linked', 'cancelled'], true)) {
|
||||
$task['cooling_active'] = false;
|
||||
$task['cooling_remaining_minutes'] = 0;
|
||||
$task['cooling_remaining_seconds'] = 0;
|
||||
$task['auto_link_at'] = null;
|
||||
$task['ready_for_autolink'] = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
$taskType = (string) ($task['task_type'] ?? '');
|
||||
$lastModified = ! empty($task['updated_at'])
|
||||
? (string) $task['updated_at']
|
||||
|
|
|
|||
|
|
@ -18,17 +18,19 @@ namespace App\Libraries\Abdm;
|
|||
*/
|
||||
class EAtriaBridgeConnector implements AbdmConnectorInterface
|
||||
{
|
||||
private string $baseUrl;
|
||||
private string $token;
|
||||
private string $hfrId;
|
||||
private string $bridgeHospitalId;
|
||||
private int $timeoutSec;
|
||||
private string $baseUrl = '';
|
||||
private string $token = '';
|
||||
private string $hfrId = '';
|
||||
private string $bridgeHospitalId = '';
|
||||
private int $timeoutSec = 30;
|
||||
private bool $sslVerify = true;
|
||||
/** @var array<int, string> */
|
||||
private array $tokenCandidates = [];
|
||||
/** @var array<string, string> */
|
||||
private array $tokenSourceByValue = [];
|
||||
private string $tokenSource = 'config';
|
||||
/** @var array<int, float> */
|
||||
protected array $linkTokenRetryDelays = [1.5, 2.0, 2.5];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
|
|
@ -259,12 +261,12 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface
|
|||
* @param array<string, mixed> $body
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function post(string $path, array $body): array
|
||||
protected function post(string $path, array $body): array
|
||||
{
|
||||
return $this->httpCall('POST', $path, $body);
|
||||
}
|
||||
|
||||
private function patch(string $path, array $body): array
|
||||
protected function patch(string $path, array $body): array
|
||||
{
|
||||
return $this->httpCall('PATCH', $path, $body);
|
||||
}
|
||||
|
|
@ -274,7 +276,7 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface
|
|||
* @param array<string, string> $extraHeaders
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function get(string $path, array $query = [], array $extraHeaders = []): array
|
||||
protected function get(string $path, array $query = [], array $extraHeaders = []): array
|
||||
{
|
||||
// e-Atria bridge expects hfr_id alongside Bearer auth for GET endpoints too.
|
||||
if ($this->hfrId !== '' && empty($query['hfr_id'])) {
|
||||
|
|
@ -1781,7 +1783,38 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface
|
|||
if ($this->hfrId !== '' && empty($payload['hfr_id'])) {
|
||||
$payload['hfr_id'] = $this->hfrId;
|
||||
}
|
||||
return $this->post('/v3/hip/link/carecontext', $payload);
|
||||
|
||||
$hasLinkId = ! empty($payload['link_token_id']);
|
||||
$delaysSec = $this->linkTokenRetryDelays;
|
||||
$maxAttempts = $hasLinkId ? count($delaysSec) + 1 : 1;
|
||||
|
||||
for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
|
||||
$result = $this->post('/v3/hip/link/carecontext', $payload);
|
||||
|
||||
if (($result['ok'] ?? 0) === 1) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Check if error is because ABDM callback hasn't populated link_token yet on the bridge
|
||||
$err = strtolower((string) ($result['error_text'] ?? $result['error'] ?? ''));
|
||||
$isTokenPending = $hasLinkId && (
|
||||
strpos($err, 'link_token') !== false &&
|
||||
(strpos($err, 'required') !== false || strpos($err, 'link-token first') !== false)
|
||||
);
|
||||
|
||||
if (! $isTokenPending || $attempt === $maxAttempts) {
|
||||
if ($isTokenPending) {
|
||||
$result['error_text'] = 'ABDM authorization token callback is still pending from the gateway. Please retry in a few seconds.';
|
||||
$result['error'] = $result['error_text'];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
$sleepSec = $delaysSec[$attempt - 1] ?? 2.0;
|
||||
usleep((int) ($sleepSec * 1000000));
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function hipGetPatientLinks(array $filters = []): array
|
||||
|
|
|
|||
|
|
@ -135,10 +135,10 @@
|
|||
</div>
|
||||
<hr/>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label>Template Action</label>
|
||||
<button id="updatereport" type="button" class="btn btn-primary">Update</button>
|
||||
<div class="col-md-12">
|
||||
<div class="form-group d-flex align-items-center gap-2">
|
||||
<button id="updatereport" type="button" class="btn btn-primary"><?= ((int) ($repo_id ?? 0) > 0) ? 'Update Template' : 'Save Template' ?></button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal" onclick="if(typeof window.closeRadiologyActionModal==='function'){window.closeRadiologyActionModal();}">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -177,8 +177,9 @@
|
|||
})();
|
||||
|
||||
$('#updatereport').click(function() {
|
||||
var btn = $(this);
|
||||
var repo_id = $('#repo_id').val();
|
||||
var input_Reportname = $('#input_Reportname').val();
|
||||
var input_Reportname = $.trim($('#input_Reportname').val() || '');
|
||||
var charge_id = $('#charge_id').val();
|
||||
var group_id = $('#input_Reporttitle').val();
|
||||
var keywords = $('#keywords').val();
|
||||
|
|
@ -188,6 +189,48 @@
|
|||
var csrf_value = $('input[name="<?= csrf_token() ?>"]').first().val() || '<?= csrf_hash() ?>';
|
||||
var modality = <?= (int) ($modality ?? 2) ?>;
|
||||
|
||||
if (input_Reportname === '') {
|
||||
if (typeof notify === 'function') {
|
||||
notify('danger', 'Validation', 'Template Name is required.');
|
||||
} else {
|
||||
alert('Template Name is required.');
|
||||
}
|
||||
$('#input_Reportname').focus();
|
||||
return;
|
||||
}
|
||||
|
||||
btn.prop('disabled', true);
|
||||
var originalBtnText = btn.text();
|
||||
btn.text('Saving...');
|
||||
|
||||
var finishSave = function(successMsg) {
|
||||
if (typeof notify === 'function') {
|
||||
notify('success', 'Saved', successMsg || 'Template saved successfully');
|
||||
}
|
||||
if (typeof window.refreshRadiologyTemplateList === 'function') {
|
||||
window.refreshRadiologyTemplateList();
|
||||
return;
|
||||
}
|
||||
if (typeof window.closeRadiologyActionModal === 'function') {
|
||||
window.closeRadiologyActionModal(function() {
|
||||
if (typeof load_form === 'function') {
|
||||
load_form('<?= base_url('Lab_Admin/report_ultrasound_list') ?>/' + modality, 'Diagnosis Template');
|
||||
} else {
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (typeof window.cleanupRadiologyModalBackdrop === 'function') {
|
||||
window.cleanupRadiologyModalBackdrop();
|
||||
}
|
||||
if (typeof load_form === 'function') {
|
||||
load_form('<?= base_url('Lab_Admin/report_ultrasound_list') ?>/' + modality, 'Diagnosis Template');
|
||||
} else {
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
if (repo_id > 0) {
|
||||
$.post('<?= base_url('Lab_Admin/report_ultrasound_update') ?>/' + modality, {
|
||||
"repo_id": repo_id,
|
||||
|
|
@ -200,15 +243,25 @@
|
|||
"Impression": Impression,
|
||||
"<?= csrf_token() ?>": csrf_value
|
||||
}, function(data) {
|
||||
if (data && Number(data.update_record || 0) === 1 && typeof window.refreshRadiologyTemplateList === 'function') {
|
||||
window.refreshRadiologyTemplateList();
|
||||
btn.prop('disabled', false).text(originalBtnText);
|
||||
if (data && Number(data.update_record || 0) === 1) {
|
||||
finishSave(data.showcontent);
|
||||
return;
|
||||
}
|
||||
|
||||
var errMsg = (data && data.showcontent) ? data.showcontent : 'Unable to update template';
|
||||
if (typeof notify === 'function') {
|
||||
notify('success', 'Saved', data.showcontent || 'Saved');
|
||||
notify('danger', 'Error', errMsg);
|
||||
} else {
|
||||
alert(errMsg);
|
||||
}
|
||||
}, 'json');
|
||||
}, 'json').fail(function() {
|
||||
btn.prop('disabled', false).text(originalBtnText);
|
||||
if (typeof notify === 'function') {
|
||||
notify('danger', 'Error', 'Network or server error while updating template.');
|
||||
} else {
|
||||
alert('Network or server error while updating template.');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
$.post('<?= base_url('Lab_Admin/report_ultrasound_insert') ?>/' + modality, {
|
||||
"repo_id": repo_id,
|
||||
|
|
@ -221,14 +274,25 @@
|
|||
"Impression": Impression,
|
||||
"<?= csrf_token() ?>": csrf_value
|
||||
}, function(data) {
|
||||
if (data.insertid > 0) {
|
||||
if (typeof load_form === 'function') {
|
||||
load_form('<?= base_url('Lab_Admin/report_ultrasound_list') ?>/' + modality, 'Diagnosis Template');
|
||||
} else {
|
||||
window.location.href = '<?= base_url('Lab_Admin/report_ultrasound_list') ?>/' + modality;
|
||||
}
|
||||
btn.prop('disabled', false).text(originalBtnText);
|
||||
if (data && (Number(data.insertid || 0) > 0 || Number(data.update_record || 0) === 1)) {
|
||||
finishSave(data.showcontent || 'Report created successfully');
|
||||
return;
|
||||
}
|
||||
}, 'json');
|
||||
var errMsg = (data && data.showcontent) ? data.showcontent : 'Unable to create template';
|
||||
if (typeof notify === 'function') {
|
||||
notify('danger', 'Error', errMsg);
|
||||
} else {
|
||||
alert(errMsg);
|
||||
}
|
||||
}, 'json').fail(function() {
|
||||
btn.prop('disabled', false).text(originalBtnText);
|
||||
if (typeof notify === 'function') {
|
||||
notify('danger', 'Error', 'Network or server error while saving template.');
|
||||
} else {
|
||||
alert('Network or server error while saving template.');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -376,6 +376,70 @@ $selectedModality = (int) ($modality ?? 2);
|
|||
});
|
||||
});
|
||||
|
||||
function cleanupRadiologyModalBackdrop() {
|
||||
document.querySelectorAll('.modal-backdrop').forEach(function(el) {
|
||||
try { el.remove(); } catch (e) {}
|
||||
});
|
||||
document.body.classList.remove('modal-open');
|
||||
document.body.style.removeProperty('overflow');
|
||||
document.body.style.removeProperty('padding-right');
|
||||
}
|
||||
window.cleanupRadiologyModalBackdrop = cleanupRadiologyModalBackdrop;
|
||||
|
||||
function closeRadiologyActionModal(callback) {
|
||||
destroyRadiologyEditors();
|
||||
var modalEl = document.getElementById('radiologyActionModal');
|
||||
|
||||
if (!modalEl) {
|
||||
cleanupRadiologyModalBackdrop();
|
||||
if (typeof callback === 'function') callback();
|
||||
return;
|
||||
}
|
||||
|
||||
var isVisible = modalEl.classList.contains('show') || document.body.classList.contains('modal-open');
|
||||
if (!isVisible) {
|
||||
cleanupRadiologyModalBackdrop();
|
||||
if (typeof callback === 'function') callback();
|
||||
return;
|
||||
}
|
||||
|
||||
var doneCalled = false;
|
||||
function done() {
|
||||
if (doneCalled) return;
|
||||
doneCalled = true;
|
||||
cleanupRadiologyModalBackdrop();
|
||||
if (typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
modalEl.addEventListener('hidden.bs.modal', function onHidden() {
|
||||
modalEl.removeEventListener('hidden.bs.modal', onHidden);
|
||||
done();
|
||||
}, { once: true });
|
||||
|
||||
setTimeout(done, 300);
|
||||
|
||||
try {
|
||||
if (window.bootstrap && window.bootstrap.Modal) {
|
||||
var modalInstance = window.bootstrap.Modal.getInstance(modalEl);
|
||||
if (modalInstance) {
|
||||
modalInstance.hide();
|
||||
} else {
|
||||
new window.bootstrap.Modal(modalEl).hide();
|
||||
}
|
||||
} else if (window.jQuery && $(modalEl).modal) {
|
||||
$(modalEl).modal('hide');
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Error hiding modal', err);
|
||||
done();
|
||||
}
|
||||
}
|
||||
window.closeRadiologyActionModal = closeRadiologyActionModal;
|
||||
|
||||
if (actionModalEl) {
|
||||
actionModalEl.addEventListener('hidden.bs.modal', function() {
|
||||
destroyRadiologyEditors();
|
||||
|
|
@ -383,6 +447,7 @@ $selectedModality = (int) ($modality ?? 2);
|
|||
if (actionModalBodyEl) {
|
||||
actionModalBodyEl.innerHTML = '<div class="text-muted">Loading...</div>';
|
||||
}
|
||||
cleanupRadiologyModalBackdrop();
|
||||
});
|
||||
} else if (window.jQuery && $('#radiologyActionModal').on) {
|
||||
$('#radiologyActionModal').on('hidden.bs.modal', function() {
|
||||
|
|
@ -391,15 +456,18 @@ $selectedModality = (int) ($modality ?? 2);
|
|||
if (actionModalBodyEl) {
|
||||
actionModalBodyEl.innerHTML = '<div class="text-muted">Loading...</div>';
|
||||
}
|
||||
cleanupRadiologyModalBackdrop();
|
||||
});
|
||||
}
|
||||
|
||||
window.refreshRadiologyTemplateList = function() {
|
||||
if (typeof load_form === 'function') {
|
||||
load_form('<?= base_url('Lab_Admin/report_ultrasound_list') ?>/<?= $selectedModality ?>', 'Diagnosis Template');
|
||||
return;
|
||||
}
|
||||
window.location.reload();
|
||||
closeRadiologyActionModal(function() {
|
||||
if (typeof load_form === 'function') {
|
||||
load_form('<?= base_url('Lab_Admin/report_ultrasound_list') ?>/<?= $selectedModality ?>', 'Diagnosis Template');
|
||||
return;
|
||||
}
|
||||
window.location.reload();
|
||||
});
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -164,8 +164,13 @@
|
|||
<div class="text-muted small"><?= esc((string) ($t['entity_id'] ?? '')) ?></div>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($bridgeSubmitted && strtolower((string) ($t['bridge_push_status'] ?? '')) === 'linked'): ?>
|
||||
<?php
|
||||
$rawTaskStatus = strtolower(trim((string) ($t['status'] ?? 'pending')));
|
||||
$isTaskLinked = in_array($rawTaskStatus, ['completed', 'linked'], true) || ($bridgeSubmitted && strtolower((string) ($t['bridge_push_status'] ?? '')) === 'linked');
|
||||
?>
|
||||
<?php if ($isTaskLinked): ?>
|
||||
<span class="badge bg-success status-pill">LINKED</span>
|
||||
<?php if ($bridgeCareContext !== ''): ?><div class="small"><code><?= esc($bridgeCareContext) ?></code></div><?php endif; ?>
|
||||
<?php elseif ($bridgeSubmitted): ?>
|
||||
<span class="badge bg-info status-pill">BRIDGE SUBMITTED</span>
|
||||
<?php if ($bridgeCareContext !== ''): ?><div class="small"><code><?= esc($bridgeCareContext) ?></code></div><?php endif; ?>
|
||||
|
|
@ -3482,8 +3487,27 @@
|
|||
var abhaId = (abhaInput ? abhaInput.value : (row.getAttribute('data-abha-id') || '')).trim();
|
||||
var patientCell = row.children[2];
|
||||
var patientName = patientCell ? (patientCell.querySelector('strong') || patientCell.children[0] || {}).textContent || '' : '';
|
||||
var taskId = parseInt(row.getAttribute('data-task-id') || '0', 10);
|
||||
var taskType = (row.getAttribute('data-task-type') || '').trim();
|
||||
var entityId = (row.getAttribute('data-entity-id') || '').trim();
|
||||
|
||||
if (typeof window.openAbdmHipLinkModal === 'function') {
|
||||
window.openAbdmHipLinkModal(patientId, abhaId, { patient_name: patientName.trim() });
|
||||
window.openAbdmHipLinkModal(patientId, abhaId, {
|
||||
patient_name: patientName.trim(),
|
||||
task_id: taskId,
|
||||
task_type: taskType,
|
||||
entity_id: entityId,
|
||||
onLinked: function (res) {
|
||||
var statusCell = row.children[5];
|
||||
if (statusCell) {
|
||||
var refHtml = '';
|
||||
if (res && res.care_contexts && res.care_contexts.length > 0) {
|
||||
refHtml = '<div class="small"><code>' + (res.care_contexts[0].ref || '') + '</code></div>';
|
||||
}
|
||||
statusCell.innerHTML = '<span class="badge bg-success status-pill">LINKED</span>' + refHtml;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -3493,8 +3517,15 @@
|
|||
var patientId = parseInt(btn.getAttribute('data-patient-id') || '0', 10);
|
||||
var abhaId = (btn.getAttribute('data-abha-id') || '').trim();
|
||||
var patientName = (btn.getAttribute('data-patient-name') || '').trim();
|
||||
var opdId = (btn.getAttribute('data-opd-id') || '').trim();
|
||||
var taskType = opdId ? 'opd_prescription_publish' : '';
|
||||
var entityId = opdId || '';
|
||||
if (typeof window.openAbdmHipLinkModal === 'function') {
|
||||
window.openAbdmHipLinkModal(patientId, abhaId, { patient_name: patientName });
|
||||
window.openAbdmHipLinkModal(patientId, abhaId, {
|
||||
patient_name: patientName,
|
||||
task_type: taskType,
|
||||
entity_id: entityId
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -265,12 +265,12 @@
|
|||
var btnDeselectAll = document.getElementById('btnDeselectAllCareContexts');
|
||||
if (btnSelectAll) {
|
||||
btnSelectAll.addEventListener('click', function() {
|
||||
document.querySelectorAll('.care-context-cb').forEach(function(cb) { cb.checked = true; });
|
||||
document.querySelectorAll('.care-context-cb:not(:disabled)').forEach(function(cb) { cb.checked = true; });
|
||||
});
|
||||
}
|
||||
if (btnDeselectAll) {
|
||||
btnDeselectAll.addEventListener('click', function() {
|
||||
document.querySelectorAll('.care-context-cb').forEach(function(cb) { cb.checked = false; });
|
||||
document.querySelectorAll('.care-context-cb:not(:disabled)').forEach(function(cb) { cb.checked = false; });
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -279,8 +279,9 @@
|
|||
if (btnReload) {
|
||||
btnReload.addEventListener('click', function() {
|
||||
var abha = (document.getElementById('hipAbhaAddress').value || '').trim();
|
||||
if (abha) {
|
||||
loadPatientCareContexts(0, abha);
|
||||
var pId = (window.currentHipTaskContext && window.currentHipTaskContext.patientId) ? window.currentHipTaskContext.patientId : 0;
|
||||
if (abha || pId > 0) {
|
||||
loadPatientCareContexts(pId, abha);
|
||||
} else {
|
||||
alert('Please enter an ABHA Address first.');
|
||||
}
|
||||
|
|
@ -304,16 +305,27 @@
|
|||
var display = item.display || ref;
|
||||
var hiType = item.hi_type || 'Record';
|
||||
var isLinked = !!item.is_linked;
|
||||
var isFhirReady = (typeof item.is_fhir_ready !== 'undefined') ? !!item.is_fhir_ready : true;
|
||||
var isPrimary = !!item.is_primary;
|
||||
|
||||
// Check if already in list
|
||||
var existing = container.querySelector('input[value="' + ref.replace(/"/g, '\\"') + '"]');
|
||||
if (existing) {
|
||||
existing.checked = true;
|
||||
if (isPrimary && !existing.disabled) {
|
||||
existing.checked = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var wrapper = document.createElement('div');
|
||||
wrapper.className = 'border rounded p-2 d-flex align-items-center justify-content-between bg-white';
|
||||
var baseClass = 'border rounded p-2 d-flex align-items-center justify-content-between ';
|
||||
if (isPrimary) {
|
||||
wrapper.className = baseClass + 'bg-primary-subtle border-primary shadow-sm';
|
||||
} else if (!isFhirReady) {
|
||||
wrapper.className = baseClass + 'bg-light border-warning-subtle text-muted';
|
||||
} else {
|
||||
wrapper.className = baseClass + 'bg-white';
|
||||
}
|
||||
|
||||
var left = document.createElement('div');
|
||||
left.className = 'form-check mb-0';
|
||||
|
|
@ -324,10 +336,29 @@
|
|||
cb.id = 'cc_' + Math.random().toString(36).substring(2, 9);
|
||||
cb.value = ref;
|
||||
cb.dataset.hiType = hiType;
|
||||
cb.checked = (typeof isChecked === 'boolean') ? isChecked : !isLinked;
|
||||
cb.dataset.isPrimary = isPrimary ? '1' : '0';
|
||||
cb.dataset.isFhirReady = isFhirReady ? '1' : '0';
|
||||
cb.dataset.isLinked = isLinked ? '1' : '0';
|
||||
|
||||
if (isLinked) {
|
||||
cb.disabled = true;
|
||||
cb.checked = false;
|
||||
} else if (!isFhirReady) {
|
||||
cb.disabled = true;
|
||||
cb.checked = false;
|
||||
cb.title = 'Incomplete findings - finalize report before linking';
|
||||
} else if (typeof isChecked === 'boolean') {
|
||||
cb.checked = isChecked;
|
||||
} else if (isPrimary) {
|
||||
cb.checked = true;
|
||||
} else {
|
||||
// If opened for a specific task, only check the primary task context by default
|
||||
var hasSpecificTask = !!(window.currentHipTaskContext && window.currentHipTaskContext.taskId);
|
||||
cb.checked = !hasSpecificTask;
|
||||
}
|
||||
|
||||
var label = document.createElement('label');
|
||||
label.className = 'form-check-label ms-2 small fw-semibold';
|
||||
label.className = 'form-check-label ms-2 small fw-semibold' + (!isFhirReady ? ' text-secondary' : '');
|
||||
label.htmlFor = cb.id;
|
||||
label.textContent = display;
|
||||
|
||||
|
|
@ -335,7 +366,14 @@
|
|||
left.appendChild(label);
|
||||
|
||||
var badgeSpan = document.createElement('div');
|
||||
badgeSpan.className = 'd-flex align-items-center gap-1';
|
||||
badgeSpan.className = 'd-flex align-items-center gap-1 flex-wrap justify-content-end';
|
||||
|
||||
if (isPrimary) {
|
||||
var primaryBadge = document.createElement('span');
|
||||
primaryBadge.className = 'badge bg-primary text-white small';
|
||||
primaryBadge.innerHTML = '<i class="bi bi-star-fill me-1"></i>Current Task';
|
||||
badgeSpan.appendChild(primaryBadge);
|
||||
}
|
||||
|
||||
var typeBadge = document.createElement('span');
|
||||
typeBadge.className = 'badge bg-secondary-subtle text-secondary border small';
|
||||
|
|
@ -344,13 +382,19 @@
|
|||
|
||||
if (isLinked) {
|
||||
var linkedBadge = document.createElement('span');
|
||||
linkedBadge.className = 'badge bg-success-subtle text-success border border-success small';
|
||||
linkedBadge.className = 'badge bg-success-subtle text-success border border-success small cc-badge-linked';
|
||||
linkedBadge.innerHTML = '<i class="bi bi-check-circle me-1"></i>Linked';
|
||||
badgeSpan.appendChild(linkedBadge);
|
||||
} else if (!isFhirReady) {
|
||||
var draftBadge = document.createElement('span');
|
||||
draftBadge.className = 'badge bg-warning-subtle text-dark border border-warning small cc-badge-draft';
|
||||
draftBadge.innerHTML = '<i class="bi bi-clock me-1"></i>Draft / Incomplete';
|
||||
draftBadge.title = 'Clinical findings must be documented before linking to ABDM';
|
||||
badgeSpan.appendChild(draftBadge);
|
||||
} else {
|
||||
var readyBadge = document.createElement('span');
|
||||
readyBadge.className = 'badge bg-primary-subtle text-primary border border-primary small';
|
||||
readyBadge.textContent = 'Ready to Link';
|
||||
readyBadge.className = 'badge bg-success-subtle text-success border border-success-subtle small cc-badge-ready';
|
||||
readyBadge.innerHTML = '<i class="bi bi-check2-circle me-1"></i>FHIR Ready';
|
||||
badgeSpan.appendChild(readyBadge);
|
||||
}
|
||||
|
||||
|
|
@ -362,16 +406,23 @@
|
|||
/**
|
||||
* Load patient demographics and care contexts from server.
|
||||
*/
|
||||
window.loadPatientCareContexts = function(patientId, abhaAddress) {
|
||||
window.loadPatientCareContexts = function(patientId, abhaAddress, taskType, entityId, taskId) {
|
||||
var loading = document.getElementById('hipCareContextsLoading');
|
||||
var container = document.getElementById('hipCareContextsContainer');
|
||||
if (loading) loading.classList.remove('d-none');
|
||||
if (container) container.innerHTML = '';
|
||||
|
||||
taskType = taskType || (window.currentHipTaskContext ? window.currentHipTaskContext.taskType : '');
|
||||
entityId = entityId || (window.currentHipTaskContext ? window.currentHipTaskContext.entityId : '');
|
||||
taskId = taskId || (window.currentHipTaskContext ? window.currentHipTaskContext.taskId : 0);
|
||||
|
||||
var csrf = getCsrfData();
|
||||
var url = '<?= base_url('AbdmGateway/hip_patient_care_contexts') ?>'
|
||||
+ '?patient_id=' + encodeURIComponent(patientId || 0)
|
||||
+ '&abha_address=' + encodeURIComponent(abhaAddress || '');
|
||||
+ '&abha_address=' + encodeURIComponent(abhaAddress || '')
|
||||
+ '&task_type=' + encodeURIComponent(taskType || '')
|
||||
+ '&entity_id=' + encodeURIComponent(entityId || '')
|
||||
+ '&task_id=' + encodeURIComponent(taskId || 0);
|
||||
|
||||
fetch(url, {
|
||||
method: 'GET',
|
||||
|
|
@ -385,14 +436,20 @@
|
|||
if (loading) loading.classList.add('d-none');
|
||||
updateCsrf(data);
|
||||
|
||||
if (data.ok && data.patient) {
|
||||
if (!data.ok) {
|
||||
var errTxt = data.error_text || data.error || (data.message || 'Failed to load patient records');
|
||||
container.innerHTML = '<div class="alert alert-warning py-2 px-3 small mb-0"><i class="bi bi-exclamation-triangle me-1"></i>' + errTxt + '</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.patient) {
|
||||
var p = data.patient;
|
||||
if (p.abha_address) document.getElementById('hipAbhaAddress').value = p.abha_address;
|
||||
var pAbhaNum = (p.abha_number || '').trim();
|
||||
if (pAbhaNum.indexOf('@') !== -1 || pAbhaNum.replace(/\D/g, '').length !== 14) {
|
||||
pAbhaNum = '';
|
||||
}
|
||||
document.getElementById('hipAbhaNumber').value = pAbhaNum;
|
||||
if (pAbhaNum) document.getElementById('hipAbhaNumber').value = pAbhaNum;
|
||||
if (p.name) document.getElementById('hipPatientName').value = p.name;
|
||||
if (p.gender) document.getElementById('hipGender').value = p.gender;
|
||||
if (p.year_of_birth) document.getElementById('hipYob').value = p.year_of_birth;
|
||||
|
|
@ -407,13 +464,55 @@
|
|||
}
|
||||
|
||||
var contexts = data.care_contexts || [];
|
||||
var currentTaskAlreadyLinked = false;
|
||||
var currentTaskLinkedRef = '';
|
||||
if (contexts.length > 0) {
|
||||
contexts.forEach(function(item) {
|
||||
appendCareContextCheckbox(item);
|
||||
if (item.is_primary && item.is_linked) {
|
||||
currentTaskAlreadyLinked = true;
|
||||
currentTaskLinkedRef = item.ref;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
container.innerHTML = '<div class="text-muted small p-3 text-center" id="hipCareContextsEmpty">No clinical visit records found for this patient. Click <b>Custom</b> to add a test context.</div>';
|
||||
}
|
||||
|
||||
if (currentTaskAlreadyLinked && window.currentHipTaskContext && window.currentHipTaskContext.taskId > 0) {
|
||||
var tId = window.currentHipTaskContext.taskId;
|
||||
var alertBox = document.getElementById('hipLinkStatusAlert');
|
||||
if (alertBox) {
|
||||
alertBox.className = 'alert alert-info py-2 px-3 small';
|
||||
alertBox.innerHTML = '<strong><i class="bi bi-info-circle-fill me-1"></i>Already Linked:</strong> The record for this task (<code>' + currentTaskLinkedRef + '</code>) is already linked to patient\'s ABHA in ABDM.';
|
||||
alertBox.classList.remove('d-none');
|
||||
}
|
||||
try {
|
||||
var csrf = getCsrfData();
|
||||
var markFormData = new URLSearchParams();
|
||||
markFormData.append('task_id', tId);
|
||||
markFormData.append('status', 'completed');
|
||||
markFormData.append('note', 'Care context already linked in ABDM: ' + currentTaskLinkedRef);
|
||||
markFormData.append(csrf.name || 'csrf_hms', csrf.hash);
|
||||
|
||||
fetch('<?= base_url('AbdmTaskBoard/mark_status') ?>', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: markFormData.toString()
|
||||
}).then(function(mRes) { return mRes.json(); }).then(function(mResult) {
|
||||
updateCsrf(mResult);
|
||||
if (window.currentHipTaskContext && typeof window.currentHipTaskContext.onLinked === 'function') {
|
||||
window.currentHipTaskContext.onLinked({
|
||||
task_id: tId,
|
||||
care_contexts: [{ ref: currentTaskLinkedRef }]
|
||||
});
|
||||
}
|
||||
}).catch(function() {});
|
||||
} catch (e) {}
|
||||
}
|
||||
})
|
||||
.catch(function(err) {
|
||||
if (loading) loading.classList.add('d-none');
|
||||
|
|
@ -426,17 +525,27 @@
|
|||
*/
|
||||
window.openAbdmHipLinkModal = function(patientId, abhaAddress, prefill) {
|
||||
prefill = prefill || {};
|
||||
window.currentHipTaskContext = {
|
||||
patientId: patientId,
|
||||
abhaAddress: abhaAddress,
|
||||
taskId: prefill.task_id || 0,
|
||||
taskType: prefill.task_type || '',
|
||||
entityId: prefill.entity_id || '',
|
||||
onLinked: prefill.onLinked || null
|
||||
};
|
||||
|
||||
var modalEl = document.getElementById('abdmHipLinkModal');
|
||||
if (!modalEl) return;
|
||||
var modal = bootstrap.Modal.getInstance(modalEl) || new bootstrap.Modal(modalEl);
|
||||
|
||||
// Reset fields
|
||||
document.getElementById('hipAbhaAddress').value = abhaAddress || prefill.abha_address || '';
|
||||
var rawPrefillNum = (prefill.abha_number || prefill.abha_id || '').trim();
|
||||
if (rawPrefillNum.indexOf('@') !== -1 || rawPrefillNum.replace(/\D/g, '').length !== 14) {
|
||||
rawPrefillNum = '';
|
||||
}
|
||||
document.getElementById('hipAbhaNumber').value = rawPrefillNum;
|
||||
var rawAbhaInput = (abhaAddress || prefill.abha_address || prefill.abha_id || '').trim();
|
||||
var isAbhaAddress = rawAbhaInput.indexOf('@') !== -1;
|
||||
var clean14Num = rawAbhaInput.replace(/\D/g, '');
|
||||
var is14Num = !isAbhaAddress && clean14Num.length === 14;
|
||||
|
||||
document.getElementById('hipAbhaAddress').value = isAbhaAddress ? rawAbhaInput : (prefill.abha_address || '');
|
||||
document.getElementById('hipAbhaNumber').value = is14Num ? clean14Num : ((prefill.abha_number || '').indexOf('@') === -1 ? (prefill.abha_number || '') : '');
|
||||
document.getElementById('hipPatientName').value = prefill.name || prefill.patient_name || '';
|
||||
document.getElementById('hipGender').value = prefill.gender || 'M';
|
||||
document.getElementById('hipYob').value = prefill.year_of_birth || prefill.yob || '';
|
||||
|
|
@ -458,7 +567,7 @@
|
|||
|
||||
// Load visits
|
||||
if (patientId > 0 || (abhaAddress && abhaAddress.length > 3)) {
|
||||
loadPatientCareContexts(patientId, abhaAddress);
|
||||
loadPatientCareContexts(patientId, abhaAddress, prefill.task_type, prefill.entity_id, prefill.task_id);
|
||||
} else {
|
||||
var container = document.getElementById('hipCareContextsContainer');
|
||||
if (container) {
|
||||
|
|
@ -505,12 +614,23 @@
|
|||
var submitBtn = document.getElementById('btnSubmitHipLink');
|
||||
|
||||
var selectedContexts = [];
|
||||
var dominantHiType = '';
|
||||
var hasIncomplete = false;
|
||||
|
||||
document.querySelectorAll('.care-context-cb:checked').forEach(function(cb) {
|
||||
var ref = cb.value;
|
||||
var display = cb.nextElementSibling ? cb.nextElementSibling.textContent : ref;
|
||||
var itemHiType = cb.dataset.hiType || '';
|
||||
if (!dominantHiType && itemHiType) {
|
||||
dominantHiType = itemHiType;
|
||||
}
|
||||
if (cb.dataset.isFhirReady === '0') {
|
||||
hasIncomplete = true;
|
||||
}
|
||||
selectedContexts.push({
|
||||
ref: ref,
|
||||
display: display
|
||||
display: display,
|
||||
hi_type: itemHiType
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -530,16 +650,57 @@
|
|||
alert('Please select at least one care context to link.');
|
||||
return;
|
||||
}
|
||||
if (hasIncomplete) {
|
||||
alert('One or more selected records are marked as incomplete. Only finalized records with documented findings can be linked to ABDM.');
|
||||
return;
|
||||
}
|
||||
|
||||
var allAlreadyLinked = selectedContexts.length > 0 && selectedContexts.every(function(c) {
|
||||
var cb = document.querySelector('.care-context-cb[value="' + c.ref.replace(/"/g, '\\"') + '"]');
|
||||
return cb && cb.closest('.border') && cb.closest('.border').querySelector('.badge.bg-success-subtle');
|
||||
return cb && cb.dataset.isLinked === '1';
|
||||
});
|
||||
if (allAlreadyLinked) {
|
||||
alertBox.className = 'alert alert-success py-2 px-3 small';
|
||||
alertBox.innerHTML = '<strong><i class="bi bi-check-circle-fill me-1"></i>Already Linked!</strong> '
|
||||
+ 'The selected care context(s) are already linked with this patient\'s ABHA. Patient can view and fetch the records directly in their PHR app.';
|
||||
alertBox.classList.remove('d-none');
|
||||
|
||||
// If initiated for a specific task board task, sync task to completed so status isn't stuck on Ready to link!
|
||||
if (window.currentHipTaskContext && window.currentHipTaskContext.taskId > 0) {
|
||||
var tId = window.currentHipTaskContext.taskId;
|
||||
var taskNote = 'Care context already linked to ABHA: ' + selectedContexts.map(function(c) { return c.ref; }).join(', ');
|
||||
try {
|
||||
var csrf = getCsrfData();
|
||||
var markFormData = new URLSearchParams();
|
||||
markFormData.append('task_id', tId);
|
||||
markFormData.append('status', 'completed');
|
||||
markFormData.append('note', taskNote);
|
||||
markFormData.append(csrf.name || 'csrf_hms', csrf.hash);
|
||||
|
||||
fetch('<?= base_url('AbdmTaskBoard/mark_status') ?>', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: markFormData.toString()
|
||||
}).then(function(mRes) { return mRes.json(); }).then(function(mResult) {
|
||||
updateCsrf(mResult);
|
||||
if (window.currentHipTaskContext && typeof window.currentHipTaskContext.onLinked === 'function') {
|
||||
window.currentHipTaskContext.onLinked({
|
||||
task_id: tId,
|
||||
care_contexts: selectedContexts
|
||||
});
|
||||
}
|
||||
}).catch(function() {});
|
||||
} catch (e) {}
|
||||
} else if (window.currentHipTaskContext && typeof window.currentHipTaskContext.onLinked === 'function') {
|
||||
window.currentHipTaskContext.onLinked({
|
||||
care_contexts: selectedContexts
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -581,10 +742,17 @@
|
|||
}
|
||||
|
||||
var linkTokenId = tokenData.link_token_id;
|
||||
alertBox.innerHTML = '<strong>Step 2/2:</strong> Link token acquired (ID: ' + linkTokenId + '). Linking ' + selectedContexts.length + ' care context(s)...';
|
||||
var isReused = tokenData.status === 'reused';
|
||||
alertBox.innerHTML = '<strong>Step 2/2:</strong> ' + (isReused ? 'Active link token verified' : 'Link token acquired') + ' (ID: ' + linkTokenId + '). Linking ' + selectedContexts.length + ' care context(s)...';
|
||||
|
||||
// Allow token callback buffer if needed
|
||||
await new Promise(function(r) { setTimeout(r, 1200); });
|
||||
// Allow token callback buffer if needed (if newly generated)
|
||||
var waitMs = isReused ? 300 : 2000;
|
||||
await new Promise(function(r) { setTimeout(r, waitMs); });
|
||||
|
||||
var hiTypeToSend = dominantHiType || 'OPConsultRecord';
|
||||
if (hiTypeToSend === 'OPConsultation') {
|
||||
hiTypeToSend = 'OPConsultRecord';
|
||||
}
|
||||
|
||||
// Step 2: Link Care Contexts
|
||||
csrf = getCsrfData();
|
||||
|
|
@ -601,7 +769,7 @@
|
|||
link_token_id: linkTokenId,
|
||||
patient_ref: abhaAddress,
|
||||
display: name,
|
||||
hi_type: 'OPConsultation',
|
||||
hi_type: hiTypeToSend,
|
||||
care_contexts: selectedContexts,
|
||||
csrf_hms: csrf.hash
|
||||
})
|
||||
|
|
@ -614,11 +782,97 @@
|
|||
alertBox.className = 'alert alert-success py-2 px-3 small';
|
||||
alertBox.innerHTML = '<strong><i class="bi bi-check-circle-fill me-1"></i>Success!</strong> '
|
||||
+ 'Care contexts successfully submitted to ABDM for linking. Patient can now discover and access them in their ABHA / PHR app.';
|
||||
|
||||
// If initiated for a specific task board task, mark the task as completed
|
||||
if (window.currentHipTaskContext && window.currentHipTaskContext.taskId > 0) {
|
||||
var tId = window.currentHipTaskContext.taskId;
|
||||
var taskNote = 'Care context linked to ABHA: ' + selectedContexts.map(function(c) { return c.ref; }).join(', ');
|
||||
try {
|
||||
csrf = getCsrfData();
|
||||
var markFormData = new URLSearchParams();
|
||||
markFormData.append('task_id', tId);
|
||||
markFormData.append('status', 'completed');
|
||||
markFormData.append('note', taskNote);
|
||||
markFormData.append(csrf.name || 'csrf_hms', csrf.hash);
|
||||
|
||||
fetch('<?= base_url('AbdmTaskBoard/mark_status') ?>', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: markFormData.toString()
|
||||
}).then(function(mRes) { return mRes.json(); }).then(function(mResult) {
|
||||
updateCsrf(mResult);
|
||||
if (window.currentHipTaskContext && typeof window.currentHipTaskContext.onLinked === 'function') {
|
||||
window.currentHipTaskContext.onLinked({
|
||||
task_id: tId,
|
||||
care_contexts: selectedContexts
|
||||
});
|
||||
}
|
||||
}).catch(function() {});
|
||||
} catch (e) {}
|
||||
} else if (window.currentHipTaskContext && typeof window.currentHipTaskContext.onLinked === 'function') {
|
||||
window.currentHipTaskContext.onLinked({
|
||||
care_contexts: selectedContexts
|
||||
});
|
||||
}
|
||||
|
||||
// Refresh care contexts list
|
||||
loadPatientCareContexts(0, abhaAddress);
|
||||
loadPatientCareContexts(
|
||||
window.currentHipTaskContext ? window.currentHipTaskContext.patientId : 0,
|
||||
abhaAddress
|
||||
);
|
||||
} else {
|
||||
var errDetail = linkData.error_text || linkData.error || (linkData.detail && linkData.detail.error && linkData.detail.error.message) || 'Care context linking request failed';
|
||||
var isAlreadyLinkedErr = /already\s+(linked|registered|added|exists)/i.test(errDetail) || /1017/i.test(errDetail);
|
||||
if (isAlreadyLinkedErr) {
|
||||
alertBox.className = 'alert alert-success py-2 px-3 small';
|
||||
alertBox.innerHTML = '<strong><i class="bi bi-check-circle-fill me-1"></i>Already Linked!</strong> '
|
||||
+ 'ABDM reports this care context is already linked to patient\'s ABHA. Task status updated to Linked.';
|
||||
|
||||
if (window.currentHipTaskContext && window.currentHipTaskContext.taskId > 0) {
|
||||
var tId = window.currentHipTaskContext.taskId;
|
||||
var taskNote = 'Care context confirmed already linked in ABDM: ' + selectedContexts.map(function(c) { return c.ref; }).join(', ');
|
||||
try {
|
||||
csrf = getCsrfData();
|
||||
var markFormData = new URLSearchParams();
|
||||
markFormData.append('task_id', tId);
|
||||
markFormData.append('status', 'completed');
|
||||
markFormData.append('note', taskNote);
|
||||
markFormData.append(csrf.name || 'csrf_hms', csrf.hash);
|
||||
|
||||
fetch('<?= base_url('AbdmTaskBoard/mark_status') ?>', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: markFormData.toString()
|
||||
}).then(function(mRes) { return mRes.json(); }).then(function(mResult) {
|
||||
updateCsrf(mResult);
|
||||
if (window.currentHipTaskContext && typeof window.currentHipTaskContext.onLinked === 'function') {
|
||||
window.currentHipTaskContext.onLinked({
|
||||
task_id: tId,
|
||||
care_contexts: selectedContexts
|
||||
});
|
||||
}
|
||||
}).catch(function() {});
|
||||
} catch (e) {}
|
||||
} else if (window.currentHipTaskContext && typeof window.currentHipTaskContext.onLinked === 'function') {
|
||||
window.currentHipTaskContext.onLinked({
|
||||
care_contexts: selectedContexts
|
||||
});
|
||||
}
|
||||
|
||||
loadPatientCareContexts(
|
||||
window.currentHipTaskContext ? window.currentHipTaskContext.patientId : 0,
|
||||
abhaAddress
|
||||
);
|
||||
return;
|
||||
}
|
||||
throw new Error(errDetail);
|
||||
}
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -304,6 +304,31 @@
|
|||
return false;
|
||||
}
|
||||
|
||||
function cleanupBootstrapModalState() {
|
||||
try {
|
||||
document.querySelectorAll('.modal.show').forEach(function(modalEl) {
|
||||
if (window.bootstrap && window.bootstrap.Modal) {
|
||||
var instance = window.bootstrap.Modal.getInstance(modalEl);
|
||||
if (instance) {
|
||||
try { instance.dispose(); } catch (e) {}
|
||||
}
|
||||
}
|
||||
modalEl.classList.remove('show');
|
||||
modalEl.style.display = 'none';
|
||||
modalEl.setAttribute('aria-hidden', 'true');
|
||||
});
|
||||
document.querySelectorAll('.modal-backdrop').forEach(function(backdrop) {
|
||||
try { backdrop.remove(); } catch (e) {}
|
||||
});
|
||||
document.body.classList.remove('modal-open');
|
||||
document.body.style.removeProperty('overflow');
|
||||
document.body.style.removeProperty('padding-right');
|
||||
} catch (e) {
|
||||
console.warn('cleanupBootstrapModalState failed', e);
|
||||
}
|
||||
}
|
||||
window.cleanupBootstrapModalState = cleanupBootstrapModalState;
|
||||
|
||||
window.load_form = function(ourl, top_title = '') {
|
||||
if (!requireJquery()) return;
|
||||
var normalizedUrl = String(ourl || '');
|
||||
|
|
@ -319,12 +344,14 @@
|
|||
console.warn('pageCleanup failed', e);
|
||||
}
|
||||
}
|
||||
cleanupBootstrapModalState();
|
||||
$.ajax({
|
||||
url: normalizedUrl,
|
||||
dataType: "html",
|
||||
async: true,
|
||||
timeout: REQUEST_TIMEOUT_MS,
|
||||
beforeSend: function() {
|
||||
cleanupBootstrapModalState();
|
||||
$('#main').html('loading...');
|
||||
$("#wait").css("display", "block");
|
||||
}
|
||||
|
|
@ -338,6 +365,7 @@
|
|||
if (mainEl) {
|
||||
mainEl.innerHTML = html;
|
||||
}
|
||||
cleanupBootstrapModalState();
|
||||
executeInjectedScripts('main');
|
||||
if (typeof window.showPageNotifications === 'function') {
|
||||
window.showPageNotifications(mainEl);
|
||||
|
|
|
|||
283
tests/unit/Abdm/CareContextDiscoveryTest.php
Normal file
283
tests/unit/Abdm/CareContextDiscoveryTest.php
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Unit\Abdm;
|
||||
|
||||
use App\Controllers\AbdmGateway;
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
use ReflectionClass;
|
||||
|
||||
final class CareContextDiscoveryTest extends CIUnitTestCase
|
||||
{
|
||||
private AbdmGateway $gateway;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$db = \Config\Database::connect();
|
||||
$forge = \Config\Database::forge();
|
||||
|
||||
$db->query("CREATE TABLE IF NOT EXISTS " . $db->prefixTable('lab_request') . " (
|
||||
id INTEGER PRIMARY KEY,
|
||||
patient_id INTEGER,
|
||||
lab_type INTEGER,
|
||||
charge_id INTEGER,
|
||||
report_name TEXT,
|
||||
Report_Data TEXT,
|
||||
report_data_Impression TEXT,
|
||||
status INTEGER,
|
||||
Request_Date TEXT,
|
||||
reported_time TEXT,
|
||||
collected_time TEXT
|
||||
)");
|
||||
|
||||
$db->query("CREATE TABLE IF NOT EXISTS " . $db->prefixTable('charge_master') . " (
|
||||
id INTEGER PRIMARY KEY,
|
||||
charge_name TEXT
|
||||
)");
|
||||
|
||||
$db->query("CREATE TABLE IF NOT EXISTS " . $db->prefixTable('opd_prescription') . " (
|
||||
id INTEGER PRIMARY KEY,
|
||||
p_id INTEGER,
|
||||
date_opd_visit TEXT,
|
||||
session_id INTEGER,
|
||||
p_datetime TEXT
|
||||
)");
|
||||
|
||||
$db->query("CREATE TABLE IF NOT EXISTS " . $db->prefixTable('health_records') . " (
|
||||
id INTEGER PRIMARY KEY,
|
||||
patient_id INTEGER,
|
||||
abha_id TEXT,
|
||||
hi_type TEXT,
|
||||
care_context_reference TEXT,
|
||||
created_at TEXT,
|
||||
updated_at TEXT
|
||||
)");
|
||||
|
||||
$db->query("CREATE TABLE IF NOT EXISTS " . $db->prefixTable('patient_master') . " (
|
||||
id INTEGER PRIMARY KEY,
|
||||
p_code TEXT,
|
||||
p_fname TEXT,
|
||||
dob TEXT,
|
||||
age TEXT,
|
||||
gender INTEGER,
|
||||
abha_id TEXT,
|
||||
abha_address TEXT,
|
||||
mphone1 TEXT
|
||||
)");
|
||||
|
||||
$db->query("CREATE TABLE IF NOT EXISTS " . $db->prefixTable('ipd_master') . " (
|
||||
id INTEGER PRIMARY KEY,
|
||||
p_id INTEGER,
|
||||
discharge_date TEXT,
|
||||
register_date TEXT,
|
||||
ipd_status INTEGER
|
||||
)");
|
||||
|
||||
$db->query("CREATE TABLE IF NOT EXISTS " . $db->prefixTable('immunization_records') . " (
|
||||
id INTEGER PRIMARY KEY,
|
||||
patient_id INTEGER,
|
||||
vaccine_name TEXT,
|
||||
given_date TEXT,
|
||||
abdm_care_context_reference TEXT
|
||||
)");
|
||||
|
||||
$db->table('charge_master')->emptyTable();
|
||||
$db->table('lab_request')->emptyTable();
|
||||
$db->table('opd_prescription')->emptyTable();
|
||||
$db->table('health_records')->emptyTable();
|
||||
$db->table('patient_master')->emptyTable();
|
||||
$db->table('ipd_master')->emptyTable();
|
||||
$db->table('immunization_records')->emptyTable();
|
||||
|
||||
$db->table('patient_master')->insert([
|
||||
'id' => 12,
|
||||
'p_code' => 'P26071000012',
|
||||
'p_fname' => 'KESHAV SINGH',
|
||||
'dob' => '1976-06-30',
|
||||
'age' => null,
|
||||
'gender' => 1,
|
||||
'abha_id' => '91747451787143',
|
||||
'abha_address' => 'singhkeshav301976@sbx',
|
||||
'mphone1' => '7817828379',
|
||||
]);
|
||||
|
||||
$db->table('charge_master')->insert([
|
||||
'id' => 19,
|
||||
'charge_name' => 'USG WHOLE ABDOMEN',
|
||||
]);
|
||||
|
||||
$db->table('lab_request')->insert([
|
||||
'id' => 31,
|
||||
'patient_id' => 12,
|
||||
'lab_type' => 6,
|
||||
'charge_id' => 19,
|
||||
'report_name' => 'USG WHOLE ABDOMEN',
|
||||
'Report_Data' => 'Liver is normal. Gall bladder normal.',
|
||||
'report_data_Impression' => 'Normal study.',
|
||||
'status' => 1,
|
||||
'Request_Date' => '2026-09-29',
|
||||
'reported_time' => '2026-09-29 11:00:00',
|
||||
]);
|
||||
|
||||
$db->table('opd_prescription')->insert([
|
||||
'id' => 101,
|
||||
'p_id' => 12,
|
||||
'date_opd_visit' => '2026-09-28',
|
||||
'session_id' => 50,
|
||||
'p_datetime' => '2026-09-28 10:00:00',
|
||||
]);
|
||||
|
||||
$this->gateway = new AbdmGateway();
|
||||
$this->gateway->initController(
|
||||
\Config\Services::request(),
|
||||
\Config\Services::response(),
|
||||
\Config\Services::logger()
|
||||
);
|
||||
}
|
||||
|
||||
public function testCareContextsDiscoveryWithTargetTask(): void
|
||||
{
|
||||
$reflector = new ReflectionClass($this->gateway);
|
||||
$method = $reflector->getMethod('findCareContextsForPatient');
|
||||
$method->setAccessible(true);
|
||||
|
||||
// Test with patient 12 and task_type='radiology_report_publish', entity_id='31'
|
||||
[$v3List, $fullList] = $method->invoke(
|
||||
$this->gateway,
|
||||
12,
|
||||
'P-12',
|
||||
'KESHAV SINGH',
|
||||
'radiology_report_publish',
|
||||
'31',
|
||||
591
|
||||
);
|
||||
|
||||
$this->assertNotEmpty($fullList, 'Care contexts list should not be empty for patient 12');
|
||||
|
||||
// Verify the primary context is the radiology report #31
|
||||
$primaryCtx = $fullList[0];
|
||||
$this->assertTrue($primaryCtx['is_primary'] ?? false, 'First care context should be marked as primary');
|
||||
$this->assertSame('DiagnosticReportRecord', $primaryCtx['record_type']);
|
||||
$this->assertStringStartsWith('RAD-31-', $primaryCtx['careContextId']);
|
||||
$this->assertTrue($primaryCtx['is_fhir_ready']);
|
||||
|
||||
// Verify that v3List also contains referenceNumber
|
||||
$this->assertSame($primaryCtx['careContextId'], $v3List[0]['referenceNumber']);
|
||||
|
||||
// Verify secondary contexts (e.g. OPD prescription) are also discovered
|
||||
$hasOpd = false;
|
||||
foreach ($fullList as $ctx) {
|
||||
if ($ctx['record_type'] === 'OPConsultRecord') {
|
||||
$hasOpd = true;
|
||||
$this->assertFalse($ctx['is_primary']);
|
||||
}
|
||||
}
|
||||
$this->assertTrue($hasOpd, 'Patient OPD consultations should also be included in care contexts');
|
||||
}
|
||||
|
||||
public function testCareContextsDiscoveryDraftStatus(): void
|
||||
{
|
||||
$db = \Config\Database::connect();
|
||||
// Insert incomplete lab request (status = 0 and empty findings)
|
||||
$db->table('lab_request')->insert([
|
||||
'id' => 99,
|
||||
'patient_id' => 12,
|
||||
'lab_type' => 1,
|
||||
'charge_id' => 0,
|
||||
'report_name' => 'Draft Blood Test',
|
||||
'Report_Data' => '',
|
||||
'report_data_Impression' => '',
|
||||
'status' => 0,
|
||||
'Request_Date' => '2026-09-29',
|
||||
]);
|
||||
|
||||
$reflector = new ReflectionClass($this->gateway);
|
||||
$method = $reflector->getMethod('findCareContextsForPatient');
|
||||
$method->setAccessible(true);
|
||||
|
||||
[$v3List, $fullList] = $method->invoke(
|
||||
$this->gateway,
|
||||
12,
|
||||
'P-12',
|
||||
'KESHAV SINGH'
|
||||
);
|
||||
|
||||
$draftCtx = null;
|
||||
foreach ($fullList as $ctx) {
|
||||
if (str_starts_with($ctx['careContextId'], 'LAB-99-')) {
|
||||
$draftCtx = $ctx;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertNotNull($draftCtx);
|
||||
$this->assertFalse($draftCtx['is_fhir_ready'], 'Incomplete lab report should not be marked FHIR ready');
|
||||
}
|
||||
|
||||
public function testHipPatientCareContextsEndpoint(): void
|
||||
{
|
||||
$_SERVER['HTTP_X_REQUESTED_WITH'] = 'xmlhttprequest';
|
||||
$_REQUEST['patient_id'] = '12';
|
||||
$_REQUEST['abha_address'] = '91747451787143';
|
||||
$_REQUEST['task_type'] = 'radiology_report_publish';
|
||||
$_REQUEST['entity_id'] = '31';
|
||||
$_REQUEST['task_id'] = '591';
|
||||
|
||||
$req = service('request');
|
||||
$req->setHeader('X-Requested-With', 'XMLHttpRequest');
|
||||
$this->gateway->initController($req, service('response'), service('logger'));
|
||||
|
||||
$res = $this->gateway->hipPatientCareContexts();
|
||||
$body = json_decode($res->getBody(), true);
|
||||
|
||||
$this->assertSame(1, $body['ok'] ?? 0);
|
||||
$this->assertNotEmpty($body['care_contexts'] ?? []);
|
||||
}
|
||||
|
||||
public function testCareContextsDiscoveryWithIpdAndImmunization(): void
|
||||
{
|
||||
$db = \Config\Database::connect();
|
||||
$db->table('ipd_master')->insert([
|
||||
'id' => 55,
|
||||
'p_id' => 12,
|
||||
'discharge_date' => '2026-09-20',
|
||||
'register_date' => '2026-09-15',
|
||||
'ipd_status' => 1,
|
||||
]);
|
||||
$db->table('immunization_records')->insert([
|
||||
'id' => 77,
|
||||
'patient_id' => 12,
|
||||
'vaccine_name' => 'Covaxin',
|
||||
'given_date' => '2026-09-18',
|
||||
'abdm_care_context_reference' => 'IMM-77-20260918',
|
||||
]);
|
||||
|
||||
$reflector = new ReflectionClass($this->gateway);
|
||||
$method = $reflector->getMethod('findCareContextsForPatient');
|
||||
$method->setAccessible(true);
|
||||
|
||||
[$v3List, $fullList] = $method->invoke(
|
||||
$this->gateway,
|
||||
12,
|
||||
'P-12',
|
||||
'KESHAV SINGH'
|
||||
);
|
||||
|
||||
$foundIpd = false;
|
||||
$foundImm = false;
|
||||
foreach ($fullList as $ctx) {
|
||||
if ($ctx['record_type'] === 'DischargeSummaryRecord') {
|
||||
$foundIpd = true;
|
||||
$this->assertStringContainsString('DISCHARGE-55-', $ctx['careContextId']);
|
||||
}
|
||||
if ($ctx['record_type'] === 'ImmunizationRecord') {
|
||||
$foundImm = true;
|
||||
$this->assertSame('IMM-77-20260918', $ctx['careContextId']);
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertTrue($foundIpd, 'IPD DischargeSummaryRecord should be discovered');
|
||||
$this->assertTrue($foundImm, 'ImmunizationRecord should be discovered');
|
||||
}
|
||||
}
|
||||
79
tests/unit/Abdm/EAtriaBridgeConnectorLinkTest.php
Normal file
79
tests/unit/Abdm/EAtriaBridgeConnectorLinkTest.php
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
<?php
|
||||
|
||||
use App\Libraries\Abdm\EAtriaBridgeConnector;
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
|
||||
final class EAtriaBridgeConnectorLinkTest extends CIUnitTestCase
|
||||
{
|
||||
public function testHipLinkCareContextRetriesWhenTokenPending(): void
|
||||
{
|
||||
$connector = $this->getMockBuilder(EAtriaBridgeConnector::class)
|
||||
->disableOriginalConstructor()
|
||||
->onlyMethods(['post'])
|
||||
->getMock();
|
||||
|
||||
// 1st call returns token pending error, 2nd call returns success
|
||||
$connector->expects($this->exactly(2))
|
||||
->method('post')
|
||||
->willReturnOnConsecutiveCalls(
|
||||
[
|
||||
'ok' => 0,
|
||||
'http_code' => 400,
|
||||
'error' => 'link_token or link_token_id is required (call /api/v3/hip/link-token first)',
|
||||
],
|
||||
[
|
||||
'ok' => 1,
|
||||
'http_code' => 202,
|
||||
'message' => 'Care-context link request sent to ABDM.',
|
||||
]
|
||||
);
|
||||
|
||||
$payload = [
|
||||
'abha_address' => 'user@sbx',
|
||||
'link_token_id' => 12345,
|
||||
'care_contexts' => [['ref' => 'CC-1', 'display' => 'Visit 1']],
|
||||
];
|
||||
|
||||
$refProp = new ReflectionProperty($connector, 'linkTokenRetryDelays');
|
||||
$refProp->setAccessible(true);
|
||||
$refProp->setValue($connector, [0]);
|
||||
|
||||
$res = $connector->hipLinkCareContext($payload);
|
||||
|
||||
$this->assertSame(1, $res['ok']);
|
||||
$this->assertSame(202, $res['http_code']);
|
||||
$this->assertSame('Care-context link request sent to ABDM.', $res['message']);
|
||||
}
|
||||
|
||||
public function testHipLinkCareContextReturnsErrorAfterMaxRetries(): void
|
||||
{
|
||||
$connector = $this->getMockBuilder(EAtriaBridgeConnector::class)
|
||||
->disableOriginalConstructor()
|
||||
->onlyMethods(['post'])
|
||||
->getMock();
|
||||
|
||||
$refProp = new ReflectionProperty($connector, 'linkTokenRetryDelays');
|
||||
$refProp->setAccessible(true);
|
||||
$refProp->setValue($connector, [0, 0, 0]);
|
||||
|
||||
// All 4 attempts return pending error
|
||||
$connector->expects($this->exactly(4))
|
||||
->method('post')
|
||||
->willReturn([
|
||||
'ok' => 0,
|
||||
'http_code' => 400,
|
||||
'error' => 'link_token or link_token_id is required (call /api/v3/hip/link-token first)',
|
||||
]);
|
||||
|
||||
$payload = [
|
||||
'abha_address' => 'user@sbx',
|
||||
'link_token_id' => 99999,
|
||||
'care_contexts' => [['ref' => 'CC-1', 'display' => 'Visit 1']],
|
||||
];
|
||||
|
||||
$res = $connector->hipLinkCareContext($payload);
|
||||
|
||||
$this->assertSame(0, $res['ok']);
|
||||
$this->assertStringContainsString('ABDM authorization token callback is still pending', $res['error_text']);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user