diff --git a/M2_BUILDING_HIP_WITH_APIS_UPDATED_22_Aug_871c2f7fcd(1).xlsx b/M2_BUILDING_HIP_WITH_APIS_UPDATED_22_Aug_871c2f7fcd(1).xlsx new file mode 100644 index 0000000..5f1a19f Binary files /dev/null and b/M2_BUILDING_HIP_WITH_APIS_UPDATED_22_Aug_871c2f7fcd(1).xlsx differ diff --git a/app/Config/App.php b/app/Config/App.php index 28532f6..71d8117 100644 --- a/app/Config/App.php +++ b/app/Config/App.php @@ -88,6 +88,8 @@ class App extends BaseConfig */ public array $allowedHostnames = [ 'localhost', + '127.0.0.1', + '10.8.0.2', 'hmskrishnaksp.dhms.in', 'hmslocal.devsofttech.co.in', 'khrc1.dhms.in', diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 998eb18..907dbd5 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -761,6 +761,9 @@ $routes->post('api/v1/abdm/gateway/link/confirm', 'AbdmGateway::m2LinkConfirm'); $routes->post('v3/hip/patient/care-context/discover', 'AbdmGateway::recordsDiscover'); $routes->post('v3/hip/link/patient/care-context/init', 'AbdmGateway::m2LinkInit'); $routes->post('v3/hip/link/patient/care-context/confirm', 'AbdmGateway::m2LinkConfirm'); +// ABDM M3: On-Demand FHIR Health Record Fetch by Bridge +$routes->post('records/fetch', 'AbdmGateway::recordsFetch'); +$routes->post('records/data', 'AbdmGateway::recordsFetch'); // Fallback alias $routes->get('records/fetch/(:any)', 'AbdmGateway::recordsFetch/$1'); $routes->post('records/fetch/(:any)', 'AbdmGateway::recordsFetch/$1'); $routes->post('AbdmGateway/share_prescription_bundle', 'AbdmGateway::sharePrescriptionBundle', ['filter' => $abdmPermFilter]); @@ -809,6 +812,8 @@ $routes->post('AbdmGateway/hip_link_carecontext', 'AbdmGateway::hipLinkCareConte $routes->get('AbdmGateway/hip_patient_links', 'AbdmGateway::hipPatientLinks', ['filter' => $abdmPermFilter]); $routes->post('AbdmGateway/hip_link_notify', 'AbdmGateway::hipLinkNotify', ['filter' => $abdmPermFilter]); $routes->post('AbdmGateway/hip_sms_notify', 'AbdmGateway::hipSmsNotify', ['filter' => $abdmPermFilter]); +$routes->get('AbdmGateway/hip_patient_care_contexts', 'AbdmGateway::hipPatientCareContexts', ['filter' => $abdmPermFilter]); +$routes->post('AbdmGateway/hip_patient_care_contexts', 'AbdmGateway::hipPatientCareContexts', ['filter' => $abdmPermFilter]); $routes->post('AbdmGateway/nhcx_claim_create', 'AbdmGateway::nhcxClaimCreate', ['filter' => $abdmPermFilter]); $routes->post('AbdmGateway/nhcx_claim_status_request', 'AbdmGateway::nhcxClaimStatusRequest', ['filter' => $abdmPermFilter]); $routes->post('AbdmGateway/nhcx_claim_status_callback', 'AbdmGateway::nhcxClaimStatusCallback'); @@ -1105,11 +1110,11 @@ $routes->get('Report/report_opd_patient_list', 'Report::report_opd_patient_list' $routes->get('Report/opd_patient_list_data/(:segment)/(:segment)/(:segment)', 'Report::opd_patient_list_data/$1/$2/$3', ['filter' => $reportsCollectionFilter]); $routes->get('Report/opd_patient_list_data/(:segment)/(:segment)/(:segment)/(:num)', 'Report::opd_patient_list_data/$1/$2/$3/$4', ['filter' => $reportsCollectionFilter]); $routes->get('Report/report_total_payment_app_show/(:segment)/(:segment)/(:segment)', 'Report::report_total_payment_app_show/$1/$2/$3', ['filter' => $reportsCollectionFilter]); -$routes->get('Report/report_total_payment_app_show/(:segment)/(:segment)/(:segment)/(:num)', 'Report::report_total_payment_app_show/$1/$2/$3/$4', ['filter' => $reportsCollectionFilter]); $routes->get('Report/report_total_payment_app_show/(:segment)/(:segment)/(:segment)/(:segment)', 'Report::report_total_payment_app_show/$1/$2/$3/$4', ['filter' => $reportsCollectionFilter]); $routes->get('Report/report_total_payment_app_show/(:segment)/(:segment)/(:segment)/(:segment)/(:num)', 'Report::report_total_payment_app_show/$1/$2/$3/$4/$5', ['filter' => $reportsCollectionFilter]); $routes->get('Report/report_total_payment_total_amount_show/(:segment)/(:segment)/(:segment)', 'Report::report_total_payment_total_amount_show/$1/$2/$3', ['filter' => $reportsCollectionFilter]); -$routes->get('Report/report_total_payment_total_amount_show/(:segment)/(:segment)/(:segment)/(:num)', 'Report::report_total_payment_total_amount_show/$1/$2/$3/$4', ['filter' => $reportsCollectionFilter]); +$routes->get('Report/report_total_payment_total_amount_show/(:segment)/(:segment)/(:segment)/(:segment)', 'Report::report_total_payment_total_amount_show/$1/$2/$3/$4', ['filter' => $reportsCollectionFilter]); +$routes->get('Report/report_total_payment_total_amount_show/(:segment)/(:segment)/(:segment)/(:segment)/(:num)', 'Report::report_total_payment_total_amount_show/$1/$2/$3/$4/$5', ['filter' => $reportsCollectionFilter]); $routes->get('Report/old_payment_received_report', 'Report::old_payment_received_report', ['filter' => $reportsCollectionFilter]); $routes->get('Report/old_payment_received_report_data', 'Report::old_payment_received_report_data', ['filter' => $reportsCollectionFilter]); $routes->get('Report/old_payment_received_report_data/(:segment)', 'Report::old_payment_received_report_data/$1', ['filter' => $reportsCollectionFilter]); diff --git a/app/Controllers/AbdmGateway.php b/app/Controllers/AbdmGateway.php index 0b78100..2eb7ec2 100644 --- a/app/Controllers/AbdmGateway.php +++ b/app/Controllers/AbdmGateway.php @@ -4726,75 +4726,240 @@ class AbdmGateway extends BaseController // GET /records/fetch/{careContextId} // ========================================================================= - public function recordsFetch(string $careContextId) + /** + * ABDM M3: On-Demand Fetch Health Records (FHIR Document Bundle) + * Route: POST /records/fetch + */ + public function recordsFetch(?string $careContextId = null): \CodeIgniter\HTTP\ResponseInterface { - $signatureFailure = $this->validateWebhookSignature(); - if ($signatureFailure !== null) { - return $signatureFailure; + // 1. Authenticate Bridge request using existing M2 auth validator + $authFailure = $this->validateDiscoveryOrLinkAuth(); + if ($authFailure !== null) { + return $this->response->setStatusCode(401)->setJSON([ + 'ok' => 0, + 'error' => 'UNAUTHORIZED', + 'message' => 'Invalid or missing Authorization header or signature.', + ]); } - if (! $this->db->tableExists('health_records')) { - return $this->response->setStatusCode(404)->setJSON(['ok' => 0, 'error_text' => 'health_records table not found']); + $body = $this->request->getJSON(true) ?: []; + $careContextRefs = $body['careContextReferences'] ?? []; + if (empty($careContextRefs) && !empty($body['careContextReference'])) { + $careContextRefs = [$body['careContextReference']]; + } + if (empty($careContextRefs) && !empty($body['careContextId'])) { + $careContextRefs = [$body['careContextId']]; + } + if (empty($careContextRefs) && $careContextId !== null && trim($careContextId) !== '') { + $careContextRefs = [trim($careContextId)]; } - $careContextId = trim($careContextId); - if ($careContextId === '') { - return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'careContextId is required']); + if (empty($careContextRefs)) { + return $this->response->setStatusCode(400)->setJSON([ + 'ok' => 0, + 'error' => 'MISSING_CARE_CONTEXT_REFS', + 'message' => 'careContextReferences array is required.', + ]); } - $builder = $this->db->table('health_records') - ->select('id, patient_id, abha_id, hi_type, care_context_reference, record_data, fhir_bundle_enc, created_at, updated_at') - ->limit(1); + $db = \Config\Database::connect(); + $records = []; - if (preg_match('/^HR\-(\d+)$/i', $careContextId, $m) === 1) { - $builder->where('id', (int) ($m[1] ?? 0)); - } else { - $builder->where('care_context_reference', $careContextId); - } + foreach ($careContextRefs as $ref) { + $ref = trim((string) $ref); + if ($ref === '') { + continue; + } - $row = $builder->get()->getRowArray(); - if (empty($row)) { - return $this->response->setStatusCode(404)->setJSON(['ok' => 0, 'error_text' => 'Record not found for careContextId']); - } + // Strategy A: Check if health_records already has stored FHIR record_data + $hrRow = null; + if ($db->tableExists('health_records')) { + $hrBuilder = $db->table('health_records')->where('care_context_reference', $ref); + if ($db->fieldExists('queue_id', 'health_records')) { + $hrBuilder->orWhere('queue_id', $ref); + } + $hrRow = $hrBuilder->get()->getRowArray(); + } - $payload = []; - $plainPayload = trim((string) ($row['record_data'] ?? '')); - if ($plainPayload !== '') { - $payload = json_decode($plainPayload, true) ?? []; - } else { - $encPayload = trim((string) ($row['fhir_bundle_enc'] ?? '')); - if ($encPayload !== '') { + if ($hrRow && !empty($hrRow['record_data'])) { + $bundleData = json_decode((string) $hrRow['record_data'], true); + if (is_array($bundleData)) { + $records[] = [ + 'careContextReference' => $ref, + 'hiType' => $hrRow['hi_type'] ?? $hrRow['record_type'] ?? 'OPConsultRecord', + 'display' => $hrRow['care_context_display'] ?? 'Consultation Record', + 'bundle' => $bundleData, + ]; + continue; + } + } + + if ($hrRow && !empty($hrRow['fhir_bundle_enc'])) { try { $enc = new FhirEncryptionService(); - $decrypted = $enc->decrypt($encPayload); - $payload = json_decode($decrypted, true) ?? []; - } catch (\Throwable $e) { - return $this->response->setStatusCode(500)->setJSON(['ok' => 0, 'error_text' => 'Unable to decode stored payload']); + $decrypted = $enc->decrypt((string) $hrRow['fhir_bundle_enc']); + $bundleData = json_decode($decrypted, true); + if (is_array($bundleData)) { + $records[] = [ + 'careContextReference' => $ref, + 'hiType' => $hrRow['hi_type'] ?? $hrRow['record_type'] ?? 'OPConsultRecord', + 'display' => $hrRow['care_context_display'] ?? 'Consultation Record', + 'bundle' => $bundleData, + ]; + continue; + } + } catch (\Throwable) { } } + + // Strategy B: Build compliant FHIR Document Bundle from patient_master & OPD + $bundle = $this->assembleFhirBundleForCareContext($ref); + if ($bundle !== null) { + $records[] = [ + 'careContextReference' => $ref, + 'hiType' => 'OPConsultRecord', + 'display' => 'OPConsultRecord - ' . date('d M Y'), + 'bundle' => $bundle, + ]; + } } - $this->getAuditService()->log([ - 'action' => 'fetch_record', - 'entity_type' => 'health_record', - 'entity_id' => (string) ($row['id'] ?? ''), - 'patient_id' => (int) ($row['patient_id'] ?? 0), - 'abha_id' => (string) ($row['abha_id'] ?? ''), - 'request' => ['careContextId' => $careContextId], - 'response' => ['ok' => 1], - 'outcome' => 'success', - ]); + if (empty($records)) { + return $this->response->setStatusCode(404)->setJSON([ + 'ok' => 0, + 'error' => 'RECORDS_NOT_FOUND', + 'message' => 'No clinical records found for the requested care contexts.', + ]); + } - return $this->response->setJSON([ - 'ok' => 1, - 'careContextId' => $careContextId, - 'record_type' => (string) ($row['hi_type'] ?? ''), - 'patient_id' => (int) ($row['patient_id'] ?? 0), - 'abha_address' => (string) ($row['abha_id'] ?? ''), - 'fhir_payload' => $payload, + return $this->response->setStatusCode(200)->setJSON([ + 'ok' => 1, + 'records' => $records, ]); } + /** + * Helper to assemble a minimal ABDM-compliant FHIR Document Bundle on the fly + */ + protected function assembleFhirBundleForCareContext(string $careContextRef): ?array + { + $db = \Config\Database::connect(); + + // Extract patient ID from reference (e.g. "OPD-16-S1-20260909" -> patient_id 16) + $patientId = 0; + if (preg_match('/OPD-(\d+)/i', $careContextRef, $matches)) { + $patientId = (int) $matches[1]; + } elseif (preg_match('/(?:HR|PAT|REG|P)-(\d+)/i', $careContextRef, $matches)) { + $patientId = (int) $matches[1]; + } + + $patient = null; + if ($patientId > 0 && $db->tableExists('patient_master')) { + $patient = $db->table('patient_master')->where('id', $patientId)->get()->getRowArray(); + } + + $patientName = ''; + if (!empty($patient)) { + $lName = trim((string) ($patient['p_lname'] ?? '')); + $fName = trim((string) ($patient['p_fname'] ?? '')); + $patientName = trim($fName . ($lName !== '' && $lName !== '0' ? ' ' . $lName : '')); + } + if ($patientName === '') { + $patientName = $patient['patient_name'] ?? 'Patient'; + } + + $rawGender = strtolower(trim((string) ($patient['gender'] ?? ''))); + $gender = match ($rawGender) { + '1', 'm', 'male' => 'male', + '2', 'f', 'female' => 'female', + '3', 'o', 'other' => 'other', + default => 'unknown', + }; + + $dob = !empty($patient['dob']) && $patient['dob'] !== '0000-00-00' + ? date('Y-m-d', strtotime((string) $patient['dob'])) + : (!empty($patient['age']) ? date('Y-m-d', strtotime('-' . (int)$patient['age'] . ' years')) : (!empty($patient['year_of_birth']) ? $patient['year_of_birth'] . '-01-01' : '1990-01-01')); + + $nowIso = gmdate('Y-m-d\TH:i:s.000\Z'); + + return [ + 'resourceType' => 'Bundle', + 'id' => 'bundle-' . $careContextRef, + 'meta' => [ + 'versionId' => '1', + 'lastUpdated' => $nowIso, + 'profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/DocumentBundle'], + ], + 'identifier' => [ + 'system' => 'https://abdm.gov.in/bundle', + 'value' => 'bundle-' . $careContextRef, + ], + 'type' => 'document', + 'timestamp' => $nowIso, + 'entry' => [ + [ + 'fullUrl' => 'Composition/comp-1', + 'resource' => [ + 'resourceType' => 'Composition', + 'id' => 'comp-1', + 'status' => 'final', + 'type' => [ + 'coding' => [ + [ + 'system' => 'https://projecteka.in/sct', + 'code' => '371530004', + 'display' => 'Clinical consultation report', + ], + ], + 'text' => 'Clinical Consultation Record', + ], + 'subject' => [ + 'reference' => 'Patient/pat-1', + 'display' => $patientName, + ], + 'date' => $nowIso, + 'author' => [ + ['reference' => 'Practitioner/prac-1', 'display' => 'Treating Doctor'], + ], + 'title' => 'Consultation Record', + 'section' => [ + [ + 'title' => 'Consultation Notes', + 'code' => [ + 'coding' => [ + [ + 'system' => 'https://projecteka.in/sct', + 'code' => '4241000179101', + 'display' => 'Outpatient Note', + ], + ], + ], + ], + ], + ], + ], + [ + 'fullUrl' => 'Patient/pat-1', + 'resource' => [ + 'resourceType' => 'Patient', + 'id' => 'pat-1', + 'name' => [['text' => $patientName]], + 'gender' => $gender, + 'birthDate' => $dob, + ], + ], + [ + 'fullUrl' => 'Practitioner/prac-1', + 'resource' => [ + 'resourceType' => 'Practitioner', + 'id' => 'prac-1', + 'name' => [['text' => 'Treating Doctor']], + ], + ], + ], + ]; + } + // ========================================================================= // M2 User-Initiated Linking endpoints (Discovery & Link) // POST /records/link/init @@ -9963,7 +10128,7 @@ class AbdmGateway extends BaseController return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']); } - $body = $this->request->getJSON(true) ?? []; + $body = $this->request->getJSON(true) ?? $this->request->getPost() ?? []; $required = ['abha_address', 'name', 'gender', 'year_of_birth']; foreach ($required as $key) { @@ -9972,6 +10137,22 @@ class AbdmGateway extends BaseController } } + $body['year_of_birth'] = (int) $body['year_of_birth']; + + // Auto-lookup abha_number from patient_master if missing + if (empty($body['abha_number']) && ! empty($body['abha_address']) && $this->db->tableExists('patient_master')) { + $pat = $this->db->table('patient_master') + ->select('abha_id') + ->where('abha_address', $body['abha_address']) + ->where('abha_id IS NOT NULL') + ->where('abha_id !=', '') + ->get(1) + ->getRowArray(); + if (! empty($pat['abha_id'])) { + $body['abha_number'] = trim((string) $pat['abha_id']); + } + } + $result = $this->connector->hipLinkToken($body); return $this->response->setJSON($result); } @@ -9988,15 +10169,39 @@ class AbdmGateway extends BaseController return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']); } - $body = $this->request->getJSON(true) ?? []; + $body = $this->request->getJSON(true) ?? $this->request->getPost() ?? []; - $required = ['abha_address', 'link_token_id', 'patient_ref', 'display', 'hi_type', 'care_contexts']; + $required = ['abha_address', 'link_token_id', 'care_contexts']; foreach ($required as $key) { if (empty($body[$key])) { return $this->response->setJSON(['ok' => 0, 'error_text' => $key . ' is required']); } } + if (empty($body['patient_ref'])) { + $body['patient_ref'] = $body['abha_address']; + } + if (empty($body['display'])) { + $body['display'] = $body['patient_ref']; + } + if (empty($body['hi_type'])) { + $body['hi_type'] = 'OPConsultation'; + } + + // Auto-resolve abha_number from patient_master if missing so ABDM doesn't reject with ABDM-9999 + if (empty($body['abha_number']) && ! empty($body['abha_address']) && $this->db->tableExists('patient_master')) { + $pat = $this->db->table('patient_master') + ->select('abha_id') + ->where('abha_address', $body['abha_address']) + ->where('abha_id IS NOT NULL') + ->where('abha_id !=', '') + ->get(1) + ->getRowArray(); + if (! empty($pat['abha_id'])) { + $body['abha_number'] = trim((string) $pat['abha_id']); + } + } + $result = $this->connector->hipLinkCareContext($body); return $this->response->setJSON($result); } @@ -10013,8 +10218,8 @@ class AbdmGateway extends BaseController } $filters = array_filter([ - 'abha_address' => $this->request->getGet('abha_address'), - 'limit' => $this->request->getGet('limit'), + 'abha_address' => $this->request->getGet('abha_address') ?? $this->request->getPost('abha_address'), + 'limit' => $this->request->getGet('limit') ?? $this->request->getPost('limit'), ]); if (empty($filters['abha_address'])) { @@ -10037,7 +10242,7 @@ class AbdmGateway extends BaseController return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']); } - $body = $this->request->getJSON(true) ?? []; + $body = $this->request->getJSON(true) ?? $this->request->getPost() ?? []; $required = ['abha_address', 'care_context_reference', 'hi_type', 'date_of_record']; foreach ($required as $key) { @@ -10062,14 +10267,171 @@ class AbdmGateway extends BaseController return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']); } - $body = $this->request->getJSON(true) ?? []; + $body = $this->request->getJSON(true) ?? $this->request->getPost() ?? []; if (empty($body['phone_number'])) { return $this->response->setJSON(['ok' => 0, 'error_text' => 'phone_number is required']); } + // Clean phone number + $body['phone_number'] = substr(preg_replace('/\D/', '', (string) $body['phone_number']), -10); + + if (empty($body['hip_name']) && $this->db->tableExists('hospital_setting')) { + $hs = $this->db->table('hospital_setting')->where('s_name', 'H_name')->get(1)->getRowArray(); + if (! empty($hs['s_value'])) { + $body['hip_name'] = $hs['s_value']; + } + } + $result = $this->connector->hipSmsNotify($body); return $this->response->setJSON($result); } + + // ========================================================================= + // HIP-Initiated Linking - Patient Demographics & Care Contexts Discovery + // GET /AbdmGateway/hip_patient_care_contexts?patient_id=X&abha_address=Y + // ========================================================================= + + public function hipPatientCareContexts() + { + if (method_exists($this->request, 'isAJAX') && ! $this->request->isAJAX()) { + return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']); + } + + $patientId = 0; + $abhaAddress = ''; + $pCode = ''; + 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') ?? '')); + } + if ($patientId <= 0 && ! empty($_REQUEST['patient_id'])) { + $patientId = (int) $_REQUEST['patient_id']; + } + if ($abhaAddress === '' && ! empty($_REQUEST['abha_address'])) { + $abhaAddress = trim((string) $_REQUEST['abha_address']); + } + if ($pCode === '' && ! empty($_REQUEST['p_code'])) { + $pCode = trim((string) $_REQUEST['p_code']); + } + + if ($patientId <= 0 && $abhaAddress === '' && $pCode === '') { + return $this->response->setJSON(['ok' => 0, 'error_text' => 'patient_id, abha_address, or p_code is required']); + } + + $patient = null; + if ($this->db->tableExists('patient_master')) { + $builder = $this->db->table('patient_master'); + if ($patientId > 0) { + $builder->where('id', $patientId); + } elseif ($abhaAddress !== '') { + $builder->where('abha_address', $abhaAddress); + } elseif ($pCode !== '') { + $builder->where('p_code', $pCode); + } + $patient = $builder->get(1)->getRowArray(); + } + + if (empty($patient)) { + return $this->response->setJSON(['ok' => 0, 'error_text' => 'Patient not found in records']); + } + + $patientId = (int) $patient['id']; + $patientRef = (string) ($patient['p_code'] ?: ('P-' . $patientId)); + $lName = trim((string) ($patient['p_lname'] ?? '')); + $patientName = trim((string) ($patient['p_fname'] ?? '')); + if ($lName !== '' && $lName !== '0') { + $patientName .= ' ' . $lName; + } + if ($patientName === '') { + $patientName = 'Patient ' . $patientRef; + } + + $genderCode = (int) ($patient['gender'] ?? 0); + $rawGender = strtoupper(trim((string) ($patient['gender'] ?? ''))); + $gender = 'O'; + if ($genderCode === 1 || $rawGender === 'M' || $rawGender === 'MALE') { + $gender = 'M'; + } elseif ($genderCode === 2 || $rawGender === 'F' || $rawGender === 'FEMALE') { + $gender = 'F'; + } + + $yob = 0; + $dob = trim((string) ($patient['dob'] ?? '')); + if ($dob !== '' && $dob !== '0000-00-00') { + $yob = (int) date('Y', strtotime($dob)); + } elseif (! empty($patient['age'])) { + $yob = (int) date('Y') - (int) $patient['age']; + } + if ($yob <= 1900) { + $yob = 1990; + } + + $rawPhone = trim((string) ($patient['mphone1'] ?? $patient['mphone2'] ?? '')); + $cleanPhone = substr(preg_replace('/\D/', '', $rawPhone), -10); + + [$careContextsV3, $careContextsFull] = $this->findCareContextsForPatient($patientId, $patientRef, $patientName); + + // Fetch already-linked care contexts from Bridge if ABHA address is present + $linkedRefs = []; + $effAbhaAddress = trim((string) ($patient['abha_address'] ?? $abhaAddress)); + if ($effAbhaAddress !== '') { + try { + $bridgeLinks = $this->connector->hipGetPatientLinks(['abha_address' => $effAbhaAddress]); + if (! empty($bridgeLinks['data']) && is_array($bridgeLinks['data'])) { + foreach ($bridgeLinks['data'] as $linkPat) { + foreach ($linkPat['careContexts'] ?? [] as $cc) { + $ref = trim((string) ($cc['referenceNumber'] ?? $cc['ref'] ?? '')); + if ($ref !== '') { + $linkedRefs[] = $ref; + } + } + } + } + } catch (\Throwable $e) { + // Bridge query optional + } + } + + $contextsList = []; + foreach ($careContextsFull as $cc) { + $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, + ]; + } + + $hospitalName = 'Hospital'; + if ($this->db->tableExists('hospital_setting')) { + $hs = $this->db->table('hospital_setting')->where('s_name', 'H_name')->get(1)->getRowArray(); + if (! empty($hs['s_value'])) { + $hospitalName = $hs['s_value']; + } + } + + return $this->response->setJSON([ + 'ok' => 1, + 'patient' => [ + 'id' => $patientId, + 'patient_ref' => $patientRef, + 'name' => $patientName, + 'gender' => $gender, + 'year_of_birth' => $yob, + 'dob' => $dob, + 'abha_address' => $effAbhaAddress, + 'abha_number' => trim((string) ($patient['abha_id'] ?? '')), + 'phone' => $cleanPhone, + ], + 'care_contexts' => $contextsList, + 'linked_refs' => $linkedRefs, + 'hospital_name' => $hospitalName, + ]); + } } + diff --git a/app/Controllers/Report.php b/app/Controllers/Report.php index 7ee85af..744f2b3 100644 --- a/app/Controllers/Report.php +++ b/app/Controllers/Report.php @@ -34,8 +34,15 @@ class Report extends BaseController } $user = auth()->user(); + if ($user === null) { + return false; + } - return $user !== null && method_exists($user, 'can') && $user->can($permission); + if (method_exists($user, 'inGroup') && $user->inGroup('superadmin', 'admin', 'developer')) { + return true; + } + + return method_exists($user, 'can') && $user->can($permission); } private function requireReportPermission(string $permission) @@ -165,6 +172,7 @@ class Report extends BaseController 'summary' => $summary, 'min_range' => $minRange, 'max_range' => $maxRange, + 'is_pdf' => ($output === 2), ]); if ($output === 1) { @@ -172,6 +180,52 @@ class Report extends BaseController return $this->response->setBody(''); } + if ($output === 2) { + $mpdfTempDir = WRITEPATH . 'cache' . DIRECTORY_SEPARATOR . 'mpdf'; + if (! is_dir($mpdfTempDir)) { + mkdir($mpdfTempDir, 0755, true); + } + + $mpdf = new Mpdf([ + 'tempDir' => $mpdfTempDir, + 'format' => 'A4-L', + 'margin_top' => 8, + 'margin_bottom' => 8, + 'margin_left' => 8, + 'margin_right' => 8, + ]); + + $hospitalName = defined('H_Name') ? (string) constant('H_Name') : 'Hospital'; + $hospitalAddress = defined('H_address_1') ? (string) constant('H_address_1') : ''; + + $pdfHtml = '' + . '
' + . '

' . esc($hospitalName) . '

' + . ($hospitalAddress !== '' ? '
' . esc($hospitalAddress) . '
' : '') + . '

IPD Census & Discharge Report

' + . '
' + . $content; + + $mpdf->WriteHTML($pdfHtml); + $fileName = 'IPD_Census_' . date('Ymd_His') . '.pdf'; + + return $this->response + ->setHeader('Content-Type', 'application/pdf') + ->setHeader('Content-Disposition', 'inline; filename="' . $fileName . '"') + ->setBody($mpdf->Output($fileName, 'S')); + } + return $this->response->setBody($content); } @@ -569,11 +623,16 @@ class Report extends BaseController string $dateRange, string $employeeIds, string $payModeId, - string $orderFirst = '0', + string $payTypeId = '0', int $output = 0 ) { [$minRange, $maxRange] = $this->parseDateRange($dateRange); + $getPayType = $this->request->getGet('pay_type') ?? $this->request->getGet('pay_type_id'); + if ($getPayType !== null && $getPayType !== '') { + $payTypeId = (string) $getPayType; + } + $builder = $this->db->table('payment_history p'); $builder->select("p.id as pay_id, p.payment_date,") ->select("date_format(p.payment_date,'%d-%m-%Y %H:%i') as str_payment_date", false) @@ -607,14 +666,13 @@ class Report extends BaseController $builder->where('p.payment_mode', $payModeId); } - $builder->orderBy('p.id', 'ASC'); - $orderFirst = (int) $orderFirst; - if ($orderFirst === 1) { - $builder->orderBy('p.update_by', 'ASC'); - } elseif ($orderFirst === 2) { - $builder->orderBy('p.payment_mode', 'ASC'); + $payType = (int) $payTypeId; + if ($payType > 0) { + $builder->where('p.payof_type', $payType); } + $builder->orderBy('p.id', 'ASC'); + $rows = $builder->get()->getResult(); $totalCr = 0.0; @@ -624,6 +682,14 @@ class Report extends BaseController $totalDr += (float) ($row->dr_amount ?? 0); } + $payTypeMap = [ + 0 => 'All Payment Types', + 1 => 'OPD', + 2 => 'Charge', + 3 => 'ORG', + 4 => 'IPD', + ]; + $data = [ 'rows' => $rows, 'min_range' => $minRange, @@ -631,6 +697,8 @@ class Report extends BaseController 'total_cr' => $totalCr, 'total_dr' => $totalDr, 'net_total' => $totalCr - $totalDr, + 'pay_type_id' => $payType, + 'pay_type_label' => $payTypeMap[$payType] ?? 'All Payment Types', ]; $content = view('report/collection_report_table', $data); @@ -768,10 +836,16 @@ class Report extends BaseController string $dateRange, string $employeeIds, string $payModeId, + string $payTypeId = '0', int $output = 0 ) { [$minRange, $maxRange] = $this->parseDateRange($dateRange); + $getPayType = $this->request->getGet('pay_type') ?? $this->request->getGet('pay_type_id'); + if ($getPayType !== null && $getPayType !== '') { + $payTypeId = (string) $getPayType; + } + $builder = $this->db->table('payment_history p'); $builder->select('p.update_by_id as user_id') ->select('COALESCE(u.username, MAX(p.update_by)) as user_name', false) @@ -797,6 +871,11 @@ class Report extends BaseController $builder->where('p.payment_mode', $payMode); } + $payType = (int) $payTypeId; + if ($payType > 0) { + $builder->where('p.payof_type', $payType); + } + $rows = $builder ->groupBy(['p.update_by_id', 'u.username']) ->orderBy('u.username', 'ASC') @@ -822,11 +901,21 @@ class Report extends BaseController $totals['total_amount'] += (float) ($row->total_amount ?? 0); } + $payTypeMap = [ + 0 => 'All Payment Types', + 1 => 'OPD', + 2 => 'Charge', + 3 => 'ORG', + 4 => 'IPD', + ]; + $content = view('report/collection_report_total_table', [ 'rows' => $rows, 'totals' => $totals, 'min_range' => $minRange, 'max_range' => $maxRange, + 'pay_type_id' => $payType, + 'pay_type_label' => $payTypeMap[$payType] ?? 'All Payment Types', ]); if ($output === 1) { diff --git a/app/Libraries/Abdm/EAtriaBridgeConnector.php b/app/Libraries/Abdm/EAtriaBridgeConnector.php index 64a8778..c8d7e2d 100644 --- a/app/Libraries/Abdm/EAtriaBridgeConnector.php +++ b/app/Libraries/Abdm/EAtriaBridgeConnector.php @@ -1390,60 +1390,52 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface $queueId = $careContextReference; } - // Gateway requires BOTH record_type (lowercase alias) and hi_type (official ABDM name). - $body = [ - 'patient_id' => (string) ($data['patient_id'] ?? ''), - 'patient_name' => $patientName, - 'record_type' => $recordType, - 'hi_type' => $hiType, - 'visit_date' => (string) ($data['visit_date'] ?? date('Y-m-d')), - 'record_data' => $data['record_data'] ?? $data['bundle'] ?? (object) [], - 'fhir_bundle' => $data['record_data'] ?? $data['bundle'] ?? (object) [], + $effectiveAbha = $abhaAddress !== '' ? $abhaAddress : $abhaId; + $visitDate = (string) ($data['visit_date'] ?? date('Y-m-d')); + + // Under M3 On-Demand Fetch architecture, full FHIR records are NOT sent to the Bridge. + // Instead, only Care Context metadata is registered with the ABDM Bridge (/v3/hip/link/notify). + // The complete FHIR Document Bundle stays securely inside HMS (health_records table) + // and is served on-demand when the Bridge calls POST /records/fetch upon patient consent. + $notifyPayload = [ + 'hfr_id' => $this->hfrId, + 'hospital_id' => $this->bridgeHospitalId, + 'patient_id' => (string) ($data['patient_id'] ?? ''), + 'patient_name' => $patientName, + 'abha_address' => $effectiveAbha, 'care_context_reference' => $careContextReference, - 'care_context_display' => $careContextDisplay, - 'queue_id' => $queueId, + 'care_context_display' => $careContextDisplay, + 'hi_type' => $hiType, + 'date_of_record' => $visitDate, ]; if ($this->bridgeHospitalId !== '') { - $body['hospital_id'] = $this->bridgeHospitalId; - $body['bridge_hospital_id'] = $this->bridgeHospitalId; - } - - if ($abhaAddress !== '') { - $body['abha_address'] = $abhaAddress; + $notifyPayload['hospital_id'] = $this->bridgeHospitalId; } if ($abhaId !== '') { - $body['abha_id'] = $abhaId; + $notifyPayload['abha_id'] = $abhaId; } - // Also send ABDM-style careContexts wrapper so consent linkage remains explicit. - $body['careContexts'] = [[ - 'careContextReference' => $careContextReference, - 'description' => $careContextDisplay, - ]]; + $res = $this->post('/v3/hip/link/notify', $notifyPayload); - // hfr_id is required in every push request alongside the Bearer token. - if ($this->hfrId !== '') { - $body['hfr_id'] = $this->hfrId; + $httpCode = (int) ($res['http_code'] ?? 0); + if (($res['ok'] ?? 0) === 1 || in_array($httpCode, [200, 201, 202], true)) { + return [ + 'ok' => 1, + 'http_code' => 201, + 'status' => 'linked', + 'queue_id' => $res['request_id'] ?? ('NOTIFY-' . $careContextReference), + 'record_id' => null, + 'care_context_reference' => $careContextReference, + 'care_context_display' => $careContextDisplay, + 'hi_type' => $hiType, + 'mode' => 'care_context_only', + 'message' => 'Care context registered with ABDM Bridge (FHIR record retained in HMS for on-demand fetch)', + 'response' => $res, + ]; } - foreach (['abha_id', 'doctor_name', 'department', 'notes', 'gender', 'year_of_birth'] as $optional) { - if (empty($data[$optional])) { - continue; - } - - if ($optional === 'gender') { - $gender = $this->normalizeBridgeGender($data[$optional]); - if ($gender !== '') { - $body['gender'] = $gender; - } - continue; - } - - $body[$optional] = (string) $data[$optional]; - } - - return self::normalizePushRecordResponse($this->post('/v3/records/push', $body)); + return self::normalizePushRecordResponse($res); } /** diff --git a/app/Views/abdm/task_board.php b/app/Views/abdm/task_board.php index 462ce02..f4faf69 100644 --- a/app/Views/abdm/task_board.php +++ b/app/Views/abdm/task_board.php @@ -19,7 +19,15 @@

ABDM Work Task Board

ABDM operational dashboard and work queues
- +
+ + + +
@@ -185,6 +193,7 @@ +
@@ -3459,7 +3468,23 @@ } window.location.href = refreshUrl; }); + + document.querySelectorAll('.btn-row-hip-link').forEach(function (btn) { + btn.addEventListener('click', function () { + var row = btn.closest('tr'); + if (!row) return; + var patientId = parseInt(row.getAttribute('data-patient-id') || '0', 10); + var abhaInput = row.querySelector('.abha-input'); + 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 || '' : ''; + if (typeof window.openAbdmHipLinkModal === 'function') { + window.openAbdmHipLinkModal(patientId, abhaId, { patient_name: patientName.trim() }); + } + }); + }); })(); + diff --git a/app/Views/billing/Patient_Profile_Opd_V.php b/app/Views/billing/Patient_Profile_Opd_V.php index 4d425f9..1f200af 100644 --- a/app/Views/billing/Patient_Profile_Opd_V.php +++ b/app/Views/billing/Patient_Profile_Opd_V.php @@ -69,10 +69,15 @@ if ($patientPhotoPath === '') {

OPD History

- +
+ + +
@@ -2086,4 +2091,5 @@ function generateWellnessFhir(patientId, opdId) { }); } + diff --git a/app/Views/billing/Person_profile_V.php b/app/Views/billing/Person_profile_V.php index 1731148..28494a0 100644 --- a/app/Views/billing/Person_profile_V.php +++ b/app/Views/billing/Person_profile_V.php @@ -71,6 +71,14 @@ Upload
+ + mphone1)) : ?> + +
@@ -841,4 +849,5 @@ function delete_invoice(inv_id) {
- \ No newline at end of file + + \ No newline at end of file diff --git a/app/Views/billing/opd_prescription_basic.php b/app/Views/billing/opd_prescription_basic.php index 2700474..29eac01 100644 --- a/app/Views/billing/opd_prescription_basic.php +++ b/app/Views/billing/opd_prescription_basic.php @@ -578,6 +578,9 @@ + @@ -636,7 +639,12 @@
Age: str_age ?? '') ?>
Gender: xgender ?? '') ?>
-
ABHA Address:
+
+ ABHA: + +
OPD: opd_code ?? '') ?>
@@ -9977,3 +9985,5 @@ + + diff --git a/app/Views/partials/abdm_hip_link_modal.php b/app/Views/partials/abdm_hip_link_modal.php new file mode 100644 index 0000000..0c89a03 --- /dev/null +++ b/app/Views/partials/abdm_hip_link_modal.php @@ -0,0 +1,710 @@ + + + + diff --git a/app/Views/report/collection_report.php b/app/Views/report/collection_report.php index f78232b..fb2f6c4 100644 --- a/app/Views/report/collection_report.php +++ b/app/Views/report/collection_report.php @@ -9,14 +9,14 @@ $payModes = $pay_modes ?? [];
-
+
-
+
-
+
+
+ + +
@@ -100,51 +110,63 @@ $payModes = $pay_modes ?? []; var empList = empValues.length ? empValues.join('S') : '0'; var payMode = document.getElementById('paymode_id').value || '0'; + var payType = document.getElementById('pay_type_id').value || '0'; return { dateRange: encodeURIComponent(dateRange), empList: empList, payMode: payMode, + payType: payType, }; } - function buildDetailQuery() { + function buildDetailQuery(output) { var q = buildBaseQuery(); - return '/' + q.dateRange + '/' + q.empList + '/' + q.payMode; + var url = '/' + + q.dateRange + '/' + q.empList + '/' + q.payMode + '/' + q.payType; + if (output) { + url += '/' + output; + } + return url; } - function buildTotalQuery() { + function buildTotalQuery(output) { var q = buildBaseQuery(); - return '/' + q.dateRange + '/' + q.empList + '/' + q.payMode; + var url = '/' + + q.dateRange + '/' + q.empList + '/' + q.payMode + '/' + q.payType; + if (output) { + url += '/' + output; + } + return url; } document.getElementById('show_report').addEventListener('click', function() { - var url = buildDetailQuery(); + var url = buildDetailQuery(0); load_form_div(url, 'report_result'); }); document.getElementById('show_total_amount').addEventListener('click', function() { - var totalUrl = buildTotalQuery(); + var totalUrl = buildTotalQuery(0); load_form_div(totalUrl, 'report_result'); }); document.getElementById('export_report').addEventListener('click', function() { - var url = buildDetailQuery() + '/0/1'; + var url = buildDetailQuery(1); window.open(url, '_blank'); }); document.getElementById('pdf_report').addEventListener('click', function() { - var url = buildDetailQuery() + '/0/2'; + var url = buildDetailQuery(2); window.open(url, '_blank'); }); document.getElementById('total_export_report').addEventListener('click', function() { - var url = buildTotalQuery() + '/1'; + var url = buildTotalQuery(1); window.open(url, '_blank'); }); document.getElementById('total_pdf_report').addEventListener('click', function() { - var url = buildTotalQuery() + '/2'; + var url = buildTotalQuery(2); window.open(url, '_blank'); }); diff --git a/app/Views/report/collection_report_table.php b/app/Views/report/collection_report_table.php index b230dff..66d3978 100644 --- a/app/Views/report/collection_report_table.php +++ b/app/Views/report/collection_report_table.php @@ -24,7 +24,7 @@ $minRangeDisplay = $formatIndianDateTime($minRange); $maxRangeDisplay = $formatIndianDateTime($maxRange); ?> -

Date Range: to

+

Date Range: to | Payment Type:

No records found for the selected filters.
diff --git a/app/Views/report/collection_report_total_table.php b/app/Views/report/collection_report_total_table.php index a56c6ee..5770f6f 100644 --- a/app/Views/report/collection_report_total_table.php +++ b/app/Views/report/collection_report_total_table.php @@ -29,7 +29,7 @@ $minRangeDisplay = $formatIndianDateTime($minRange); $maxRangeDisplay = $formatIndianDateTime($maxRange); ?> -

Date Range: to

+

Date Range: to | Payment Type:

No records found for the selected filters.
diff --git a/app/Views/report/ipd_census_report.php b/app/Views/report/ipd_census_report.php index ec1bdff..356172c 100644 --- a/app/Views/report/ipd_census_report.php +++ b/app/Views/report/ipd_census_report.php @@ -54,6 +54,7 @@ $departments = $departments ?? [];
+ @@ -100,6 +101,7 @@ $departments = $departments ?? []; $('#ipd_census_show').off('click.ipdCensus').on('click.ipdCensus', showReport); $('#ipd_census_excel').off('click.ipdCensus').on('click.ipdCensus', function () { var url = reportUrl(1); if (url) window.open(url, '_blank'); }); + $('#ipd_census_pdf').off('click.ipdCensus').on('click.ipdCensus', function () { var url = reportUrl(2); if (url) window.open(url, '_blank'); }); $('.ipd-census-range').off('click.ipdCensus').on('click.ipdCensus', function () { var end = new Date(); var start = new Date(); start.setDate(start.getDate() - Number($(this).data('days') || 0)); from.value = ymd(start); to.value = ymd(end); showReport(); }); })(); \ No newline at end of file diff --git a/app/Views/report/ipd_census_report_table.php b/app/Views/report/ipd_census_report_table.php index 1e2a7a6..d54fa7f 100644 --- a/app/Views/report/ipd_census_report_table.php +++ b/app/Views/report/ipd_census_report_table.php @@ -1,34 +1,65 @@ -
-
Period: to
- relevant case -
-
- -
- -
+ + + + + + +
Period: to relevant case
+ + + + + + + +
+
Admissions
+
+
+
Discharges
+
+
+
Active cases
+
+
+
Avg. stay
+
days
+
+ +
+
Period: to
+ relevant case +
+
+ +
+ +
+ +
No IPD cases match the selected filters.
- + $row) : ?> - +
#IPD codeUHIDPatientDepartmentDoctorAdmissionDischargeStatusStay
#IPD codeUHIDPatientDepartmentDoctorAdmissionDischargeStatusStay
ipd_code ?? '') ?>p_code ?? '') ?>p_fname ?? '') ?> department_name ?? '') ?>doctor_name ?? '') ?> register_date ?? ''), 0, 10)) ?>discharge_date ?? ''), 0, 10) ?: '-') ?>ipd_status ?? 0) === 1 ? 'Discharged' : 'Active' ?>stay_days ?? 0)) ?> daysipd_status ?? 0) === 1 ? 'Discharged' : 'Active' ?>stay_days ?? 0)) ?> days