feat: add IPD census PDF, collection payment filter, fix report panel visibility, and implement ABDM HIP linking

- Reports:
  - Add PDF export option with A4-L print layout for IPD Census & Discharge Report
  - Add Payment Type filter (OPD, Charge, IPD, ORG, All) to Collection Report with URL routing and summary table updates
  - Ensure superadmin/admin/developer visibility in Report Panel controller permission checks
- ABDM:
  - Implement HIP patient care-context linking and discovery integration
  - Add on-demand FHIR health record fetch endpoints for Bridge connector
  - Add ABDM HIP link modal view and wire to patient/person profiles and prescription
This commit is contained in:
DevSoftTech Server 2026-09-15 23:21:30 +05:30
parent f2e27828bb
commit dd1acb88e1
16 changed files with 1412 additions and 147 deletions

View File

@ -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',

View File

@ -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]);

View File

@ -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,
]);
}
}

View File

@ -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 = '<style>'
. 'body{font-family:Arial,sans-serif;color:#111;font-size:11px;}'
. 'table{width:100%;border-collapse:collapse;}'
. 'th,td{border:1px solid #9aa4ad;padding:5px 6px;font-size:11px;}'
. 'th{text-align:left;background:#f5f6f7;font-weight:bold;}'
. '.text-end{text-align:right;}'
. '.text-center{text-align:center;}'
. '.summary-table td{border:1px solid #ccd0d4;padding:8px;text-align:center;}'
. '.badge{display:inline-block;padding:2px 6px;font-size:10px;font-weight:bold;border-radius:3px;}'
. '.badge-success{background:#198754;color:#fff;}'
. '.badge-warning{background:#ffc107;color:#000;}'
. '</style>'
. '<div style="text-align:center;margin-bottom:10px;border-bottom:1px solid #ccc;padding-bottom:6px;">'
. '<h2 style="margin:0;font-size:16px;">' . esc($hospitalName) . '</h2>'
. ($hospitalAddress !== '' ? '<div style="font-size:10px;color:#555;">' . esc($hospitalAddress) . '</div>' : '')
. '<h3 style="margin:4px 0 0 0;font-size:13px;color:#333;">IPD Census &amp; Discharge Report</h3>'
. '</div>'
. $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) {

View File

@ -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);
}
/**

View File

@ -19,7 +19,15 @@
<h4 class="mb-0">ABDM Work Task Board</h4>
<div class="small text-muted">ABDM operational dashboard and work queues</div>
</div>
<button type="button" class="btn btn-sm btn-outline-primary" id="btnRefresh">Refresh</button>
<div class="d-flex align-items-center gap-2">
<button type="button" class="btn btn-sm btn-outline-success fw-semibold" onclick="openAbdmHipLinkModal()">
<i class="bi bi-link-45deg"></i> Link Records to ABHA (HIP)
</button>
<button type="button" class="btn btn-sm btn-outline-info fw-semibold" onclick="openAbdmHipSmsModal()">
<i class="bi bi-chat-dots"></i> Deep Link SMS (sms/notify2)
</button>
<button type="button" class="btn btn-sm btn-outline-primary" id="btnRefresh">Refresh</button>
</div>
</div>
<div class="d-flex flex-wrap gap-2 mb-3" id="taskFilters">
@ -185,6 +193,7 @@
<?php if ($showPreview): ?>
<button type="button" class="btn btn-sm btn-outline-primary preview-fhir-btn">Preview FHIR</button>
<?php endif; ?>
<button type="button" class="btn btn-sm btn-outline-success btn-row-hip-link" title="HIP-Initiated Care Context Linking"><i class="bi bi-link-45deg"></i> Link</button>
<button type="button" class="btn btn-sm btn-outline-dark close-btn">Close</button>
</div>
</td>
@ -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() });
}
});
});
})();
</script>
<?= view('partials/abdm_hip_link_modal') ?>
</body>
</html>

View File

@ -69,10 +69,15 @@ if ($patientPhotoPath === '') {
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
<h3 class="card-title mb-0">OPD History</h3>
<button type="button" class="btn btn-outline-secondary btn-sm"
onclick="load_form('<?= esc($backUrl, 'js') ?>','<?= esc($backTitle, 'js') ?>')">
Back
</button>
<div class="d-flex align-items-center gap-2">
<button type="button" class="btn btn-outline-primary btn-sm fw-semibold" onclick="openAbdmHipLinkModal(<?= (int)$patient->id ?>, '<?= esc($patientAbhaAddress ?? '') ?>', { name: '<?= esc($patient->p_fname . ' ' . ($patient->p_lname ?? '')) ?>', gender: '<?= $patient->gender == 1 ? 'M' : ($patient->gender == 2 ? 'F' : 'O') ?>', yob: '<?= !empty($patient->dob) ? date('Y', strtotime($patient->dob)) : '' ?>', phone: '<?= esc($patient->mphone1 ?? '') ?>', abha_number: '<?= esc($patient->abha_id ?? '') ?>' })">
<i class="bi bi-link-45deg me-1"></i>Link to ABHA (HIP)
</button>
<button type="button" class="btn btn-outline-secondary btn-sm"
onclick="load_form('<?= esc($backUrl, 'js') ?>','<?= esc($backTitle, 'js') ?>')">
Back
</button>
</div>
</div>
<div class="card-body">
<div class="opd-patient-meta-card" style="border:1px solid #dee2e6;border-radius:.5rem;padding:.75rem;margin-bottom:1rem;background:#fff;">
@ -2086,4 +2091,5 @@ function generateWellnessFhir(patientId, opdId) {
});
}
</script>
<?= view('partials/abdm_hip_link_modal') ?>

View File

@ -71,6 +71,14 @@
<i class="bi bi-upload me-1"></i>Upload
</button>
</div>
<button type="button" class="btn btn-outline-primary btn-sm w-100 mt-2 fw-semibold" onclick="openAbdmHipLinkModal(<?= (int)$data[0]->id ?>, '<?= esc($abhaAddress) ?>', { name: '<?= esc($data[0]->p_fname . ' ' . ($data[0]->p_lname ?? '')) ?>', gender: '<?= $data[0]->gender == 1 ? 'M' : ($data[0]->gender == 2 ? 'F' : 'O') ?>', yob: '<?= !empty($data[0]->dob) ? date('Y', strtotime($data[0]->dob)) : '' ?>', phone: '<?= esc($data[0]->mphone1 ?? '') ?>', abha_number: '<?= esc($patientAbhaId) ?>' })">
<i class="bi bi-link-45deg me-1"></i>Link Records to ABHA (HIP)
</button>
<?php if (empty($abhaAddress) && !empty($data[0]->mphone1)) : ?>
<button type="button" class="btn btn-outline-info btn-sm w-100 mt-1" onclick="openAbdmHipSmsModal('<?= esc($data[0]->mphone1) ?>')">
<i class="bi bi-chat-dots me-1"></i>Send Deep Link SMS (ABHA App)
</button>
<?php endif; ?>
</div>
</div>
<div class="mt-3 w-100">
@ -841,4 +849,5 @@ function delete_invoice(inv_id) {
</div>
</div>
<?= view('partials/abha_otp_modal') ?>
<?= view('partials/abha_otp_modal') ?>
<?= view('partials/abdm_hip_link_modal') ?>

View File

@ -578,6 +578,9 @@
<button type="button" class="btn btn-outline-success btn-sm text-start" id="btn_local_clinical_assist" title="Local rule-based support using complaints + vitals">Clinical Assist (Local)</button>
<button type="button" class="btn btn-outline-primary btn-sm text-start" id="btn_ai_full_draft" title="Use complete OPD data to generate draft notes">AI Draft (Full Form)</button>
<button type="button" class="btn btn-outline-primary btn-sm text-start" id="btn_quick_preview_fhir">Preview FHIR JSON</button>
<button type="button" class="btn btn-outline-success btn-sm text-start fw-semibold" onclick="openAbdmHipLinkModal(<?= (int)($patient_master[0]->id ?? 0) ?>, $('#abha_address').val(), { name: '<?= esc($patientName) ?>', gender: '<?= esc($patient_master[0]->gender == 1 ? 'M' : ($patient_master[0]->gender == 2 ? 'F' : 'O')) ?>', yob: '<?= !empty($patient_master[0]->dob) ? date('Y', strtotime($patient_master[0]->dob)) : '' ?>', phone: '<?= esc($patient_master[0]->mphone1 ?? '') ?>', abha_number: '<?= esc($patient_master[0]->abha_id ?? '') ?>' })">
<i class="bi bi-link-45deg me-1"></i>Link Records to ABHA (HIP)
</button>
</div>
</div>
</div>
@ -636,7 +639,12 @@
<div class="rx-meta-line"><strong>Age:</strong> <?= esc($patient_master[0]->str_age ?? '') ?></div>
<div class="rx-meta-line"><strong>Gender:</strong> <?= esc($patient_master[0]->xgender ?? '') ?></div>
<?php if ($patientAbhaAddress !== '') : ?>
<div class="rx-meta-line"><strong>ABHA Address:</strong> <?= esc($patientAbhaAddress) ?></div>
<div class="rx-meta-line d-flex align-items-center justify-content-between">
<span><strong>ABHA:</strong> <?= esc($patientAbhaAddress) ?></span>
<button type="button" class="btn btn-outline-success btn-xs py-0 px-2" style="font-size:0.75rem;" title="Link Records to ABHA (Method 4)" onclick="openAbdmHipLinkModal(<?= (int)($patient_master[0]->id ?? 0) ?>, $('#abha_address').val(), { name: '<?= esc($patientName) ?>', gender: '<?= esc($patient_master[0]->gender == 1 ? 'M' : ($patient_master[0]->gender == 2 ? 'F' : 'O')) ?>', yob: '<?= !empty($patient_master[0]->dob) ? date('Y', strtotime($patient_master[0]->dob)) : '' ?>', phone: '<?= esc($patient_master[0]->mphone1 ?? '') ?>', abha_number: '<?= esc($patient_master[0]->abha_id ?? '') ?>' })">
<i class="bi bi-link-45deg"></i> Link
</button>
</div>
<?php endif; ?>
<input type="hidden" id="abha_address" value="<?= esc($patientAbhaAddress) ?>">
<div class="rx-meta-line"><strong>OPD:</strong> <?= esc($opd_master[0]->opd_code ?? '') ?></div>
@ -9977,3 +9985,5 @@
</div>
</div>
</div>
<?= view('partials/abdm_hip_link_modal') ?>

View File

@ -0,0 +1,710 @@
<!-- ABDM M2: HIP-Initiated Linking Modal (Method 4 Demographic Auth & Deep-Link SMS) -->
<div class="modal fade" id="abdmHipLinkModal" tabindex="-1" aria-labelledby="abdmHipLinkModalTitle" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content shadow-lg border-0">
<div class="modal-header bg-primary text-white py-3">
<h5 class="modal-title fw-bold" id="abdmHipLinkModalTitle">
<i class="bi bi-link-45deg me-2 fs-5"></i>Link Hospital Records to ABHA (HIP)
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<!-- Tab Navigation -->
<ul class="nav nav-tabs px-3 pt-2 bg-light border-bottom" id="abdmHipModalTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active fw-semibold" id="tab-hip-link" data-bs-toggle="tab" data-bs-target="#pane-hip-link" type="button" role="tab" aria-selected="true">
<i class="bi bi-person-check me-1 text-primary"></i> Demographic Auth (Method 4)
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link fw-semibold" id="tab-hip-sms" data-bs-toggle="tab" data-bs-target="#pane-hip-sms" type="button" role="tab" aria-selected="false">
<i class="bi bi-chat-dots me-1 text-info"></i> Deep Link SMS (sms/notify2)
</button>
</li>
</ul>
<div class="modal-body p-4">
<div class="tab-content" id="abdmHipModalTabContent">
<!-- ========================================================================= -->
<!-- TAB 1: Method 4 Demographic Auth Care Context Linking -->
<!-- ========================================================================= -->
<div class="tab-pane fade show active" id="pane-hip-link" role="tabpanel">
<div class="alert alert-light border border-info-subtle d-flex align-items-center mb-3 py-2 px-3 small">
<i class="bi bi-info-circle-fill text-primary fs-5 me-2 flex-shrink-0"></i>
<div>
<strong>ABDM V3 HIECM Standard (Method 4):</strong> Hospital submits patient demographics to gateway. On successful verification, ABDM generates a JWT Link Token used to link hospital care contexts directly.
</div>
</div>
<!-- Patient Demographics Form -->
<div class="card mb-3 border-light-subtle bg-light-subtle">
<div class="card-header bg-white py-2 d-flex justify-content-between align-items-center">
<span class="fw-bold small text-uppercase text-secondary">
<i class="bi bi-person-badge me-1"></i> Patient Demographics (Verified for Token)
</span>
<span id="hipPatientUhidBadge" class="badge bg-secondary">UHID: -</span>
</div>
<div class="card-body py-3">
<div class="row g-2 mb-2">
<div class="col-md-7">
<label class="form-label small fw-semibold" for="hipAbhaAddress">ABHA Address (Health ID) <span class="text-danger">*</span></label>
<div class="input-group input-group-sm">
<span class="input-group-text"><i class="bi bi-at"></i></span>
<input type="text" id="hipAbhaAddress" class="form-control" placeholder="username@sbx" autocomplete="off">
<button class="btn btn-outline-secondary" type="button" id="btnReloadCareContexts" title="Reload patient records from HMS & Bridge">
<i class="bi bi-arrow-repeat"></i>
</button>
</div>
</div>
<div class="col-md-5">
<label class="form-label small fw-semibold" for="hipAbhaNumber">14-Digit ABHA Number</label>
<input type="text" id="hipAbhaNumber" class="form-control form-control-sm" placeholder="14-digit number (auto-resolved)" maxlength="17">
</div>
</div>
<div class="row g-2">
<div class="col-md-6">
<label class="form-label small fw-semibold" for="hipPatientName">Full Name <span class="text-danger">*</span></label>
<input type="text" id="hipPatientName" class="form-control form-control-sm" placeholder="Patient full name">
</div>
<div class="col-md-3">
<label class="form-label small fw-semibold" for="hipGender">Gender <span class="text-danger">*</span></label>
<select id="hipGender" class="form-select form-select-sm">
<option value="M">Male (M)</option>
<option value="F">Female (F)</option>
<option value="O">Other (O)</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label small fw-semibold" for="hipYob">Birth Year <span class="text-danger">*</span></label>
<input type="number" id="hipYob" class="form-control form-control-sm" placeholder="YYYY" min="1900" max="2026">
</div>
</div>
</div>
</div>
<!-- Care Contexts Selection -->
<div class="card mb-3 border-light-subtle">
<div class="card-header bg-white py-2 d-flex justify-content-between align-items-center flex-wrap gap-1">
<span class="fw-bold small text-uppercase text-secondary">
<i class="bi bi-journal-medical me-1"></i> Hospital Care Contexts
</span>
<div class="d-flex align-items-center gap-2">
<button type="button" class="btn btn-sm btn-link p-0 text-decoration-none small" id="btnSelectAllCareContexts">Select All</button>
<span class="text-muted">|</span>
<button type="button" class="btn btn-sm btn-link p-0 text-decoration-none small text-secondary" id="btnDeselectAllCareContexts">Deselect All</button>
<span class="text-muted">|</span>
<button type="button" class="btn btn-sm btn-link p-0 text-decoration-none small text-primary" id="btnToggleCustomContext">
<i class="bi bi-plus-circle me-1"></i>Custom
</button>
</div>
</div>
<div class="card-body p-2" style="max-height: 240px; overflow-y: auto;">
<!-- Loading Spinner -->
<div id="hipCareContextsLoading" class="text-center py-3 d-none">
<div class="spinner-border spinner-border-sm text-primary me-1" role="status"></div>
<span class="small text-muted">Loading patient care contexts...</span>
</div>
<!-- Dynamic Checkboxes Container -->
<div id="hipCareContextsContainer" class="d-flex flex-column gap-2">
<div class="text-muted small p-2 text-center" id="hipCareContextsEmpty">
No care contexts loaded. Enter an ABHA address or patient ID above to load visits.
</div>
</div>
<!-- Custom Care Context Input Form (collapsible) -->
<div id="hipCustomContextBox" class="border rounded p-2 mt-2 bg-light d-none">
<div class="small fw-bold mb-1 text-primary"><i class="bi bi-plus-square me-1"></i>Add Custom Care Context</div>
<div class="row g-2">
<div class="col-md-6">
<input type="text" id="hipCustomRef" class="form-control form-control-sm" placeholder="Reference (e.g. OPD-1024)">
</div>
<div class="col-md-4">
<input type="text" id="hipCustomDisplay" class="form-control form-control-sm" placeholder="Display name">
</div>
<div class="col-md-2">
<button type="button" class="btn btn-sm btn-primary w-100" id="btnAddCustomContext">Add</button>
</div>
</div>
</div>
</div>
</div>
<!-- Status Alert Box -->
<div id="hipLinkStatusAlert" class="alert alert-info py-2 px-3 small d-none" role="alert"></div>
</div>
<!-- ========================================================================= -->
<!-- TAB 2: Deep Link SMS Notification (sms/notify2) -->
<!-- ========================================================================= -->
<div class="tab-pane fade" id="pane-hip-sms" role="tabpanel">
<div class="alert alert-light border border-info-subtle d-flex align-items-center mb-3 py-2 px-3 small">
<i class="bi bi-phone-vibrate text-info fs-5 me-2 flex-shrink-0"></i>
<div>
<strong>NHA M2 Test Requirement (Row 56 / sms/notify2):</strong> When a patient registers with a phone number only and does not have an ABHA address, the hospital notifies HIE-CM. The patient receives an official SMS with deep links to download the PHR app and discover their hospital records.
</div>
</div>
<div class="card border-light-subtle bg-light-subtle p-3 mb-3">
<div class="mb-3">
<label class="form-label small fw-semibold" for="hipSmsPhone">Patient Mobile Number <span class="text-danger">*</span></label>
<div class="input-group input-group-sm">
<span class="input-group-text"><i class="bi bi-telephone"></i> +91</span>
<input type="tel" id="hipSmsPhone" class="form-control" placeholder="10-digit mobile number" maxlength="10">
</div>
<div class="form-text">Must be a valid 10-digit Indian mobile number.</div>
</div>
<div class="mb-2">
<label class="form-label small fw-semibold" for="hipSmsHipName">Hospital / Facility Display Name</label>
<input type="text" id="hipSmsHipName" class="form-control form-control-sm" placeholder="Hospital Name">
<div class="form-text">Included in the SMS message sent to the patient.</div>
</div>
</div>
<!-- SMS Status Alert Box -->
<div id="hipSmsStatusAlert" class="alert alert-info py-2 px-3 small d-none" role="alert"></div>
</div>
</div>
</div>
<div class="modal-footer bg-light py-2 px-3 d-flex justify-content-between align-items-center">
<button type="button" class="btn btn-sm btn-outline-secondary" id="btnCheckPatientLinks" title="Query Bridge & ABDM registry for all care contexts already linked to this ABHA">
<i class="bi bi-search me-1"></i>View Linked Records
</button>
<div class="d-flex gap-2">
<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-sm btn-success fw-semibold" id="btnSubmitHipLink" onclick="executeHipInitiatedLinking()">
<i class="bi bi-shield-lock-fill me-1"></i>Verify & Link Records
</button>
<button type="button" class="btn btn-sm btn-info text-white fw-semibold d-none" id="btnSubmitHipSms" onclick="executeHipSmsNotify()">
<i class="bi bi-send-fill me-1"></i>Send ABDM Deep-Link SMS
</button>
</div>
</div>
</div>
</div>
</div>
<script>
(function() {
// CSRF helper
function getCsrfData() {
var tokenInput = document.querySelector('input[name="csrf_token_name"]')
|| document.querySelector('input[name="csrf_hms"]')
|| document.querySelector('input[name="<?= csrf_token() ?>"]');
var name = tokenInput ? tokenInput.name : '<?= csrf_token() ?>';
var hash = tokenInput ? tokenInput.value : '<?= csrf_hash() ?>';
if (typeof window.csrfHash !== 'undefined' && window.csrfHash) hash = window.csrfHash;
if (typeof window.csrfName !== 'undefined' && window.csrfName) name = window.csrfName;
return { name: name, hash: hash };
}
function updateCsrf(res) {
if (res && res.csrfHash) {
window.csrfHash = res.csrfHash;
if (res.csrfName) window.csrfName = res.csrfName;
var inputs = document.querySelectorAll('input[name="' + (res.csrfName || 'csrf_hms') + '"]');
inputs.forEach(function(i) { i.value = res.csrfHash; });
}
}
// Switch buttons based on active tab
var modalTabs = document.getElementById('abdmHipModalTabs');
if (modalTabs) {
modalTabs.addEventListener('shown.bs.tab', function(e) {
var btnLink = document.getElementById('btnSubmitHipLink');
var btnSms = document.getElementById('btnSubmitHipSms');
if (e.target.id === 'tab-hip-sms') {
if (btnLink) btnLink.classList.add('d-none');
if (btnSms) btnSms.classList.remove('d-none');
} else {
if (btnLink) btnLink.classList.remove('d-none');
if (btnSms) btnSms.classList.add('d-none');
}
});
}
// Toggle Custom Context Input
var btnToggleCustom = document.getElementById('btnToggleCustomContext');
var boxCustom = document.getElementById('hipCustomContextBox');
if (btnToggleCustom && boxCustom) {
btnToggleCustom.addEventListener('click', function() {
boxCustom.classList.toggle('d-none');
});
}
// Add Custom Context to Container
var btnAddCustom = document.getElementById('btnAddCustomContext');
if (btnAddCustom) {
btnAddCustom.addEventListener('click', function() {
var ref = (document.getElementById('hipCustomRef').value || '').trim();
var disp = (document.getElementById('hipCustomDisplay').value || '').trim();
if (!ref) {
alert('Please enter a care context reference (e.g. OPD-1024).');
return;
}
if (!disp) disp = ref;
appendCareContextCheckbox({
ref: ref,
display: disp,
hi_type: 'OPConsultRecord',
is_linked: false
}, true);
document.getElementById('hipCustomRef').value = '';
document.getElementById('hipCustomDisplay').value = '';
boxCustom.classList.add('d-none');
});
}
// Select / Deselect All
var btnSelectAll = document.getElementById('btnSelectAllCareContexts');
var btnDeselectAll = document.getElementById('btnDeselectAllCareContexts');
if (btnSelectAll) {
btnSelectAll.addEventListener('click', function() {
document.querySelectorAll('.care-context-cb').forEach(function(cb) { cb.checked = true; });
});
}
if (btnDeselectAll) {
btnDeselectAll.addEventListener('click', function() {
document.querySelectorAll('.care-context-cb').forEach(function(cb) { cb.checked = false; });
});
}
// Reload Care Contexts on button click or blur
var btnReload = document.getElementById('btnReloadCareContexts');
if (btnReload) {
btnReload.addEventListener('click', function() {
var abha = (document.getElementById('hipAbhaAddress').value || '').trim();
if (abha) {
loadPatientCareContexts(0, abha);
} else {
alert('Please enter an ABHA Address first.');
}
});
}
// View Linked Records button
var btnCheckLinks = document.getElementById('btnCheckPatientLinks');
if (btnCheckLinks) {
btnCheckLinks.addEventListener('click', function() {
window.checkAbdmLinkedRecords();
});
}
function appendCareContextCheckbox(item, isChecked) {
var container = document.getElementById('hipCareContextsContainer');
var emptyMsg = document.getElementById('hipCareContextsEmpty');
if (emptyMsg) emptyMsg.remove();
var ref = item.ref || '';
var display = item.display || ref;
var hiType = item.hi_type || 'Record';
var isLinked = !!item.is_linked;
// Check if already in list
var existing = container.querySelector('input[value="' + ref.replace(/"/g, '\\"') + '"]');
if (existing) {
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 left = document.createElement('div');
left.className = 'form-check mb-0';
var cb = document.createElement('input');
cb.type = 'checkbox';
cb.className = 'form-check-input care-context-cb';
cb.id = 'cc_' + Math.random().toString(36).substring(2, 9);
cb.value = ref;
cb.dataset.hiType = hiType;
cb.checked = (typeof isChecked === 'boolean') ? isChecked : !isLinked;
var label = document.createElement('label');
label.className = 'form-check-label ms-2 small fw-semibold';
label.htmlFor = cb.id;
label.textContent = display;
left.appendChild(cb);
left.appendChild(label);
var badgeSpan = document.createElement('div');
badgeSpan.className = 'd-flex align-items-center gap-1';
var typeBadge = document.createElement('span');
typeBadge.className = 'badge bg-secondary-subtle text-secondary border small';
typeBadge.textContent = hiType;
badgeSpan.appendChild(typeBadge);
if (isLinked) {
var linkedBadge = document.createElement('span');
linkedBadge.className = 'badge bg-success-subtle text-success border border-success small';
linkedBadge.innerHTML = '<i class="bi bi-check-circle me-1"></i>Linked';
badgeSpan.appendChild(linkedBadge);
} else {
var readyBadge = document.createElement('span');
readyBadge.className = 'badge bg-primary-subtle text-primary border border-primary small';
readyBadge.textContent = 'Ready to Link';
badgeSpan.appendChild(readyBadge);
}
wrapper.appendChild(left);
wrapper.appendChild(badgeSpan);
container.appendChild(wrapper);
}
/**
* Load patient demographics and care contexts from server.
*/
window.loadPatientCareContexts = function(patientId, abhaAddress) {
var loading = document.getElementById('hipCareContextsLoading');
var container = document.getElementById('hipCareContextsContainer');
if (loading) loading.classList.remove('d-none');
if (container) container.innerHTML = '';
var csrf = getCsrfData();
var url = '<?= base_url('AbdmGateway/hip_patient_care_contexts') ?>'
+ '?patient_id=' + encodeURIComponent(patientId || 0)
+ '&abha_address=' + encodeURIComponent(abhaAddress || '');
fetch(url, {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json'
}
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (loading) loading.classList.add('d-none');
updateCsrf(data);
if (data.ok && data.patient) {
var p = data.patient;
if (p.abha_address) document.getElementById('hipAbhaAddress').value = p.abha_address;
if (p.abha_number) document.getElementById('hipAbhaNumber').value = p.abha_number;
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;
if (p.phone) document.getElementById('hipSmsPhone').value = p.phone;
if (p.patient_ref) {
var uhidBadge = document.getElementById('hipPatientUhidBadge');
if (uhidBadge) uhidBadge.textContent = 'UHID: ' + p.patient_ref;
}
}
if (data.hospital_name) {
document.getElementById('hipSmsHipName').value = data.hospital_name;
}
var contexts = data.care_contexts || [];
if (contexts.length > 0) {
contexts.forEach(function(item) {
appendCareContextCheckbox(item);
});
} 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>';
}
})
.catch(function(err) {
if (loading) loading.classList.add('d-none');
container.innerHTML = '<div class="text-danger small p-2 text-center">Failed to load care contexts: ' + err.message + '</div>';
});
};
/**
* Public helper to open Link Modal with patient prefill.
*/
window.openAbdmHipLinkModal = function(patientId, abhaAddress, prefill) {
prefill = prefill || {};
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 || '';
document.getElementById('hipAbhaNumber').value = prefill.abha_number || prefill.abha_id || '';
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 || '';
document.getElementById('hipSmsPhone').value = prefill.phone || prefill.mphone1 || '';
var alertBox = document.getElementById('hipLinkStatusAlert');
if (alertBox) { alertBox.classList.add('d-none'); alertBox.innerHTML = ''; }
var smsAlert = document.getElementById('hipSmsStatusAlert');
if (smsAlert) { smsAlert.classList.add('d-none'); smsAlert.innerHTML = ''; }
// Switch to Tab 1
var linkTabTrigger = document.getElementById('tab-hip-link');
if (linkTabTrigger) {
var tab = new bootstrap.Tab(linkTabTrigger);
tab.show();
}
modal.show();
// Load visits
if (patientId > 0 || (abhaAddress && abhaAddress.length > 3)) {
loadPatientCareContexts(patientId, abhaAddress);
} else {
var container = document.getElementById('hipCareContextsContainer');
if (container) {
container.innerHTML = '<div class="text-muted small p-3 text-center" id="hipCareContextsEmpty">Please enter ABHA address above or select a patient to view visits.</div>';
}
}
};
/**
* Public helper to open modal directly on Tab 2 (Deep Link SMS).
*/
window.openAbdmHipSmsModal = function(phone, hipName) {
var modalEl = document.getElementById('abdmHipLinkModal');
if (!modalEl) return;
var modal = bootstrap.Modal.getInstance(modalEl) || new bootstrap.Modal(modalEl);
if (phone) document.getElementById('hipSmsPhone').value = phone;
if (hipName) document.getElementById('hipSmsHipName').value = hipName;
var smsTabTrigger = document.getElementById('tab-hip-sms');
if (smsTabTrigger) {
var tab = new bootstrap.Tab(smsTabTrigger);
tab.show();
}
modal.show();
};
/**
* Execute 2-step HIP-Initiated Linking via Method 4.
*/
window.executeHipInitiatedLinking = async function() {
var abhaAddress = document.getElementById('hipAbhaAddress').value.trim();
var abhaNumber = document.getElementById('hipAbhaNumber').value.trim();
var name = document.getElementById('hipPatientName').value.trim();
var gender = document.getElementById('hipGender').value;
var yobVal = document.getElementById('hipYob').value.trim();
var yearOfBirth = parseInt(yobVal, 10);
var alertBox = document.getElementById('hipLinkStatusAlert');
var submitBtn = document.getElementById('btnSubmitHipLink');
var selectedContexts = [];
document.querySelectorAll('.care-context-cb:checked').forEach(function(cb) {
var ref = cb.value;
var display = cb.nextElementSibling ? cb.nextElementSibling.textContent : ref;
selectedContexts.push({
ref: ref,
display: display
});
});
if (!abhaAddress) {
alert('Please provide patient ABHA address (e.g. user@sbx).');
return;
}
if (!name) {
alert('Please provide patient name.');
return;
}
if (isNaN(yearOfBirth) || yearOfBirth < 1900) {
alert('Please provide a valid 4-digit birth year.');
return;
}
if (selectedContexts.length === 0) {
alert('Please select at least one care context to link.');
return;
}
var origBtnHtml = submitBtn.innerHTML;
submitBtn.disabled = true;
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Verifying...';
alertBox.className = 'alert alert-info py-2 px-3 small';
alertBox.innerHTML = '<strong>Step 1/2:</strong> Requesting Link Token from ABDM Gateway via Demographic Auth...';
alertBox.classList.remove('d-none');
try {
var csrf = getCsrfData();
// Step 1: Request Link Token
var tokenRes = await fetch('<?= base_url('AbdmGateway/hip_link_token') ?>', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json'
},
body: JSON.stringify({
abha_address: abhaAddress,
abha_number: abhaNumber,
name: name,
gender: gender,
year_of_birth: yearOfBirth,
csrf_hms: csrf.hash
})
});
var tokenData = await tokenRes.json();
updateCsrf(tokenData);
if (!tokenData.ok) {
var err = tokenData.error_text || tokenData.error || (tokenData.detail && tokenData.detail.error && tokenData.detail.error.message) || 'Failed to request link token';
throw new Error(err);
}
var linkTokenId = tokenData.link_token_id;
alertBox.innerHTML = '<strong>Step 2/2:</strong> 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); });
// Step 2: Link Care Contexts
csrf = getCsrfData();
var linkRes = await fetch('<?= base_url('AbdmGateway/hip_link_carecontext') ?>', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json'
},
body: JSON.stringify({
abha_address: abhaAddress,
abha_number: abhaNumber,
link_token_id: linkTokenId,
patient_ref: abhaAddress,
display: name,
hi_type: 'OPConsultation',
care_contexts: selectedContexts,
csrf_hms: csrf.hash
})
});
var linkData = await linkRes.json();
updateCsrf(linkData);
if (linkData.ok) {
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.';
// Refresh care contexts list
loadPatientCareContexts(0, abhaAddress);
} else {
var errDetail = linkData.error_text || linkData.error || (linkData.detail && linkData.detail.error && linkData.detail.error.message) || 'Care context linking request failed';
throw new Error(errDetail);
}
} catch (err) {
alertBox.className = 'alert alert-danger py-2 px-3 small';
alertBox.innerHTML = '<strong><i class="bi bi-exclamation-triangle-fill me-1"></i>Error:</strong> ' + err.message;
} finally {
submitBtn.disabled = false;
submitBtn.innerHTML = origBtnHtml;
}
};
/**
* Dispatch Deep Link SMS (sms/notify2).
*/
window.executeHipSmsNotify = async function() {
var phone = document.getElementById('hipSmsPhone').value.trim();
var hipName = document.getElementById('hipSmsHipName').value.trim();
var alertBox = document.getElementById('hipSmsStatusAlert');
var submitBtn = document.getElementById('btnSubmitHipSms');
if (!phone || phone.length < 10) {
alert('Please enter a valid 10-digit mobile number.');
return;
}
var origBtnHtml = submitBtn.innerHTML;
submitBtn.disabled = true;
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Sending SMS...';
alertBox.className = 'alert alert-info py-2 px-3 small';
alertBox.innerHTML = 'Dispatching deep-link SMS notification request to ABDM HIE-CM...';
alertBox.classList.remove('d-none');
try {
var csrf = getCsrfData();
var res = await fetch('<?= base_url('AbdmGateway/hip_sms_notify') ?>', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json'
},
body: JSON.stringify({
phone_number: phone,
hip_name: hipName,
csrf_hms: csrf.hash
})
});
var data = await res.json();
updateCsrf(data);
if (data.ok) {
alertBox.className = 'alert alert-success py-2 px-3 small';
alertBox.innerHTML = '<strong><i class="bi bi-check-circle-fill me-1"></i>SMS Dispatched!</strong> '
+ (data.message || 'Deep-link SMS request accepted by ABDM. The patient will receive an SMS to download the ABHA app.')
+ (data.request_id ? ' <span class="text-muted">(Req: ' + data.request_id + ')</span>' : '');
} else {
throw new Error(data.error_text || data.error || 'Failed to dispatch deep-link SMS');
}
} catch (err) {
alertBox.className = 'alert alert-danger py-2 px-3 small';
alertBox.innerHTML = '<strong><i class="bi bi-exclamation-triangle-fill me-1"></i>Error:</strong> ' + err.message;
} finally {
submitBtn.disabled = false;
submitBtn.innerHTML = origBtnHtml;
}
};
/**
* Check already linked care contexts from ABDM registry.
*/
window.checkAbdmLinkedRecords = function() {
var abha = (document.getElementById('hipAbhaAddress').value || '').trim();
if (!abha) {
alert('Please enter an ABHA Address first.');
return;
}
var btn = document.getElementById('btnCheckPatientLinks');
var origHtml = btn ? btn.innerHTML : '';
if (btn) {
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Checking...';
}
fetch('<?= base_url('AbdmGateway/hip_patient_links') ?>?abha_address=' + encodeURIComponent(abha), {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json'
}
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (btn) { btn.disabled = false; btn.innerHTML = origHtml; }
var records = (res.data && res.data[0] && res.data[0].careContexts) ? res.data[0].careContexts : [];
if (records.length === 0) {
alert('No care contexts currently linked to ' + abha + ' on ABDM Bridge.');
} else {
var list = records.map(function(c, i) {
return (i + 1) + '. ' + (c.referenceNumber || c.ref) + ' (' + (c.display || 'Record') + ') - Status: ' + (c.status || 'linked');
}).join('\n');
alert('Found ' + records.length + ' care context(s) linked to ' + abha + ':\n\n' + list);
}
})
.catch(function(e) {
if (btn) { btn.disabled = false; btn.innerHTML = origHtml; }
alert('Failed to check linked records: ' + e.message);
});
};
})();
</script>

View File

@ -9,14 +9,14 @@ $payModes = $pay_modes ?? [];
</div>
<div class="card-body">
<div class="row g-3 align-items-end">
<div class="col-md-5">
<div class="col-lg-4 col-md-6">
<label class="form-label">Payment Date Range</label>
<div class="d-flex gap-2">
<input type="datetime-local" class="form-control" id="report_start">
<input type="datetime-local" class="form-control" id="report_end">
</div>
</div>
<div class="col-md-5">
<div class="col-lg-3 col-md-6">
<label class="form-label">Employee Name</label>
<select class="form-control select2" id="emp_name_id" name="emp_name_id" multiple data-placeholder="Select Employees">
<option value="0">All Employees</option>
@ -25,7 +25,7 @@ $payModes = $pay_modes ?? [];
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2">
<div class="col-lg-2 col-md-6">
<label class="form-label">Payment Mode</label>
<select class="form-control select2" id="paymode_id" name="paymode_id" data-placeholder="Select Payment Mode">
<option value="0">Cash &amp; Bank</option>
@ -34,6 +34,16 @@ $payModes = $pay_modes ?? [];
<?php endforeach; ?>
</select>
</div>
<div class="col-lg-3 col-md-6">
<label class="form-label">Payment Type</label>
<select class="form-control select2" id="pay_type_id" name="pay_type_id" data-placeholder="Select Payment Type">
<option value="0">All Payment Types</option>
<option value="1">OPD</option>
<option value="2">Charge</option>
<option value="4">IPD</option>
<option value="3">ORG</option>
</select>
</div>
</div>
<div class="mt-3 d-flex flex-wrap gap-2">
@ -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 '<?= base_url('Report/report_total_payment_app_show') ?>/' + q.dateRange + '/' + q.empList + '/' + q.payMode;
var url = '<?= base_url('Report/report_total_payment_app_show') ?>/'
+ q.dateRange + '/' + q.empList + '/' + q.payMode + '/' + q.payType;
if (output) {
url += '/' + output;
}
return url;
}
function buildTotalQuery() {
function buildTotalQuery(output) {
var q = buildBaseQuery();
return '<?= base_url('Report/report_total_payment_total_amount_show') ?>/' + q.dateRange + '/' + q.empList + '/' + q.payMode;
var url = '<?= base_url('Report/report_total_payment_total_amount_show') ?>/'
+ 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');
});

View File

@ -24,7 +24,7 @@ $minRangeDisplay = $formatIndianDateTime($minRange);
$maxRangeDisplay = $formatIndianDateTime($maxRange);
?>
<p class="mb-2"><strong>Date Range:</strong> <?= esc($minRangeDisplay) ?> to <?= esc($maxRangeDisplay) ?></p>
<p class="mb-2"><strong>Date Range:</strong> <?= esc($minRangeDisplay) ?> to <?= esc($maxRangeDisplay) ?><?php if (! empty($pay_type_label) && $pay_type_label !== 'All Payment Types') : ?> | <strong>Payment Type:</strong> <span class="badge bg-primary text-white"><?= esc($pay_type_label) ?></span><?php endif; ?></p>
<?php if (empty($rows)) : ?>
<div class="text-muted">No records found for the selected filters.</div>

View File

@ -29,7 +29,7 @@ $minRangeDisplay = $formatIndianDateTime($minRange);
$maxRangeDisplay = $formatIndianDateTime($maxRange);
?>
<p class="mb-2"><strong>Date Range:</strong> <?= esc($minRangeDisplay) ?> to <?= esc($maxRangeDisplay) ?></p>
<p class="mb-2"><strong>Date Range:</strong> <?= esc($minRangeDisplay) ?> to <?= esc($maxRangeDisplay) ?><?php if (! empty($pay_type_label) && $pay_type_label !== 'All Payment Types') : ?> | <strong>Payment Type:</strong> <span class="badge bg-primary text-white"><?= esc($pay_type_label) ?></span><?php endif; ?></p>
<?php if (empty($rows)) : ?>
<div class="text-muted">No records found for the selected filters.</div>

View File

@ -54,6 +54,7 @@ $departments = $departments ?? [];
<div class="d-flex flex-wrap gap-2 mt-3">
<button type="button" class="btn btn-primary" id="ipd_census_show"><i class="bi bi-search me-1"></i>Show report</button>
<button type="button" class="btn btn-outline-success" id="ipd_census_excel"><i class="bi bi-file-earmark-excel me-1"></i>Excel</button>
<button type="button" class="btn btn-outline-danger" id="ipd_census_pdf"><i class="bi bi-file-earmark-pdf me-1"></i>PDF</button>
<button type="button" class="btn btn-outline-secondary ipd-census-range" data-days="0">Today</button>
<button type="button" class="btn btn-outline-secondary ipd-census-range" data-days="6">7 days</button>
<button type="button" class="btn btn-outline-secondary ipd-census-range" data-days="29">30 days</button>
@ -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(); });
})();
</script>

View File

@ -1,34 +1,65 @@
<?php
$rows = $rows ?? [];
$summary = $summary ?? [];
$isPdf = $is_pdf ?? false;
?>
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-3">
<div><strong>Period:</strong> <?= esc($min_range ?? '') ?> to <?= esc($max_range ?? '') ?></div>
<span class="text-muted small"><?= count($rows) ?> relevant case<?= count($rows) === 1 ? '' : 's' ?></span>
</div>
<div class="row g-2 mb-3">
<?php foreach ([
['Admissions', (int) ($summary['admissions'] ?? 0), 'primary'],
['Discharges', (int) ($summary['discharges'] ?? 0), 'success'],
['Active cases', (int) ($summary['active_cases'] ?? 0), 'warning'],
['Avg. stay', number_format((float) ($summary['average_stay'] ?? 0), 1) . ' days', 'info'],
] as [$label, $value, $tone]) : ?>
<div class="col-6 col-lg-3"><div class="border rounded p-2 bg-light"><div class="text-muted small"><?= esc($label) ?></div><div class="fs-5 fw-semibold text-<?= esc($tone) ?>"><?= esc((string) $value) ?></div></div></div>
<?php endforeach; ?>
</div>
<?php if ($isPdf) : ?>
<table style="width:100%; margin-bottom:8px; border:none;">
<tr>
<td style="border:none; padding:0; font-size:11px;"><strong>Period:</strong> <?= esc($min_range ?? '') ?> to <?= esc($max_range ?? '') ?></td>
<td style="border:none; padding:0; text-align:right; font-size:11px; color:#555;"><?= count($rows) ?> relevant case<?= count($rows) === 1 ? '' : 's' ?></td>
</tr>
</table>
<table class="summary-table" style="width:100%; margin-bottom:12px; border-collapse:collapse;">
<tr>
<td style="width:25%; background:#f8f9fa;">
<div style="font-size:10px; color:#666;">Admissions</div>
<div style="font-size:15px; font-weight:bold; color:#0d6efd;"><?= (int) ($summary['admissions'] ?? 0) ?></div>
</td>
<td style="width:25%; background:#f8f9fa;">
<div style="font-size:10px; color:#666;">Discharges</div>
<div style="font-size:15px; font-weight:bold; color:#198754;"><?= (int) ($summary['discharges'] ?? 0) ?></div>
</td>
<td style="width:25%; background:#f8f9fa;">
<div style="font-size:10px; color:#666;">Active cases</div>
<div style="font-size:15px; font-weight:bold; color:#b58105;"><?= (int) ($summary['active_cases'] ?? 0) ?></div>
</td>
<td style="width:25%; background:#f8f9fa;">
<div style="font-size:10px; color:#666;">Avg. stay</div>
<div style="font-size:15px; font-weight:bold; color:#0dcaf0;"><?= number_format((float) ($summary['average_stay'] ?? 0), 1) ?> days</div>
</td>
</tr>
</table>
<?php else : ?>
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-3">
<div><strong>Period:</strong> <?= esc($min_range ?? '') ?> to <?= esc($max_range ?? '') ?></div>
<span class="text-muted small"><?= count($rows) ?> relevant case<?= count($rows) === 1 ? '' : 's' ?></span>
</div>
<div class="row g-2 mb-3">
<?php foreach ([
['Admissions', (int) ($summary['admissions'] ?? 0), 'primary'],
['Discharges', (int) ($summary['discharges'] ?? 0), 'success'],
['Active cases', (int) ($summary['active_cases'] ?? 0), 'warning'],
['Avg. stay', number_format((float) ($summary['average_stay'] ?? 0), 1) . ' days', 'info'],
] as [$label, $value, $tone]) : ?>
<div class="col-6 col-lg-3"><div class="border rounded p-2 bg-light"><div class="text-muted small"><?= esc($label) ?></div><div class="fs-5 fw-semibold text-<?= esc($tone) ?>"><?= esc((string) $value) ?></div></div></div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if (empty($rows)) : ?>
<div class="alert alert-info mb-0">No IPD cases match the selected filters.</div>
<?php else : ?>
<table class="table table-sm table-striped table-bordered align-middle mb-0">
<thead class="table-light"><tr><th>#</th><th>IPD code</th><th>UHID</th><th>Patient</th><th>Department</th><th>Doctor</th><th>Admission</th><th>Discharge</th><th>Status</th><th class="text-end">Stay</th></tr></thead>
<thead class="table-light"><tr><th style="width:30px;">#</th><th>IPD code</th><th>UHID</th><th>Patient</th><th>Department</th><th>Doctor</th><th>Admission</th><th>Discharge</th><th style="text-align:center;">Status</th><th class="text-end">Stay</th></tr></thead>
<tbody>
<?php foreach ($rows as $index => $row) : ?>
<tr>
<td><?= $index + 1 ?></td><td><?= esc($row->ipd_code ?? '') ?></td><td><?= esc($row->p_code ?? '') ?></td><td><?= esc($row->p_fname ?? '') ?></td>
<td><?= esc($row->department_name ?? '') ?></td><td><?= esc($row->doctor_name ?? '') ?></td>
<td><?= esc(substr((string) ($row->register_date ?? ''), 0, 10)) ?></td><td><?= esc(substr((string) ($row->discharge_date ?? ''), 0, 10) ?: '-') ?></td>
<td><span class="badge text-bg-<?= (int) ($row->ipd_status ?? 0) === 1 ? 'success' : 'warning' ?>"><?= (int) ($row->ipd_status ?? 0) === 1 ? 'Discharged' : 'Active' ?></span></td><td class="text-end"><?= max(0, (int) ($row->stay_days ?? 0)) ?> days</td>
<td style="text-align:center;"><span class="badge <?= $isPdf ? ((int) ($row->ipd_status ?? 0) === 1 ? 'badge-success' : 'badge-warning') : ('text-bg-' . ((int) ($row->ipd_status ?? 0) === 1 ? 'success' : 'warning')) ?>"><?= (int) ($row->ipd_status ?? 0) === 1 ? 'Discharged' : 'Active' ?></span></td><td class="text-end"><?= max(0, (int) ($row->stay_days ?? 0)) ?> days</td>
</tr>
<?php endforeach; ?>
</tbody>