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:
parent
f2e27828bb
commit
dd1acb88e1
BIN
M2_BUILDING_HIP_WITH_APIS_UPDATED_22_Aug_871c2f7fcd(1).xlsx
Normal file
BIN
M2_BUILDING_HIP_WITH_APIS_UPDATED_22_Aug_871c2f7fcd(1).xlsx
Normal file
Binary file not shown.
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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 & 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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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') ?>
|
||||
|
||||
|
|
|
|||
|
|
@ -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') ?>
|
||||
|
|
@ -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') ?>
|
||||
|
|
|
|||
710
app/Views/partials/abdm_hip_link_modal.php
Normal file
710
app/Views/partials/abdm_hip_link_modal.php
Normal 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>
|
||||
|
|
@ -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 & 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');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user