feat(abdm & reports): align ABDM FHIR R4 Wellness & HealthDocument generators, add NABH IPD scanner, balance column in invoice list, and old payment received report
This commit is contained in:
parent
63ee0a19fd
commit
599c4806bb
|
|
@ -571,6 +571,17 @@ $routes->get('patient/abdm_consent_requests/(:num)', 'Patient::abdm_consent_requ
|
|||
$routes->get('patient/abdm_check_live_status/(:num)', 'Patient::abdm_check_live_status/$1');
|
||||
$routes->post('patient/abdm_content_request_custom/(:num)', 'Patient::abdm_content_request_custom/$1');
|
||||
$routes->get('patient/abdm_timeline/(:num)', 'Patient::abdm_timeline/$1');
|
||||
$routes->get('DoctorDocument/wellness_record_fhir_preview', 'DoctorDocument::wellness_record_fhir_preview');
|
||||
$routes->get('DoctorDocument/wellness_record_fhir_preview/(:num)', 'DoctorDocument::wellness_record_fhir_preview/$1');
|
||||
$routes->get('DoctorDocument/wellness_record_fhir_preview/(:num)/(:num)', 'DoctorDocument::wellness_record_fhir_preview/$1/$2');
|
||||
$routes->post('patient/generate_wellness_fhir/(:num)', 'Patient::generate_wellness_fhir/$1');
|
||||
$routes->post('patient/generate_wellness_fhir/(:num)/(:num)', 'Patient::generate_wellness_fhir/$1/$2');
|
||||
$routes->post('billing/patient/generate_wellness_fhir/(:num)', 'Patient::generate_wellness_fhir/$1');
|
||||
$routes->post('billing/patient/generate_wellness_fhir/(:num)/(:num)', 'Patient::generate_wellness_fhir/$1/$2');
|
||||
$routes->post('patient/generate_fhir_for_file/(:num)', 'Patient::generate_fhir_for_file/$1');
|
||||
$routes->post('patient/generate_fhir_bundle_for_all_files/(:num)', 'Patient::generate_fhir_bundle_for_all_files/$1');
|
||||
$routes->post('billing/patient/generate_fhir_for_file/(:num)', 'Patient::generate_fhir_for_file/$1');
|
||||
$routes->post('billing/patient/generate_fhir_bundle_for_all_files/(:num)', 'Patient::generate_fhir_bundle_for_all_files/$1');
|
||||
$routes->post('patient/save_profile_image/(:num)', 'Patient::save_profile_image/$1');
|
||||
$routes->match(['GET', 'POST'], 'patient/patient_file_upload/(:num)', 'Patient::patient_file_upload/$1');
|
||||
$routes->get('patient/patient_file_list/(:num)', 'Patient::patient_file_list/$1');
|
||||
|
|
@ -764,6 +775,8 @@ $routes->post('AbdmGateway/share_health_document_bundle', 'AbdmGateway::shareHea
|
|||
$routes->post('AbdmGateway/share_invoice_bundle', 'AbdmGateway::shareInvoiceBundle', ['filter' => $abdmPermFilter]);
|
||||
$routes->get('AbdmGateway/invoice_fhir_preview', 'AbdmGateway::invoiceFhirPreview', ['filter' => $abdmPermFilter]);
|
||||
$routes->post('AbdmGateway/share_invoice_source_bundle', 'AbdmGateway::shareInvoiceSourceBundle', ['filter' => $abdmPermFilter]);
|
||||
$routes->post('AbdmTaskBoard/perform_action', 'AbdmTaskBoard::performAction', ['filter' => $abdmPermFilter]);
|
||||
$routes->post('AbdmTaskBoard/performAction', 'AbdmTaskBoard::performAction', ['filter' => $abdmPermFilter]);
|
||||
$routes->get('Immunization', 'Immunization::index', ['filter' => 'permission:doctor_work.immunization.access']);
|
||||
$routes->get('Immunization/schedule_master', 'Immunization::scheduleMaster', ['filter' => 'permission:doctor_work.immunization.schedule-manage']);
|
||||
$routes->post('Immunization/sync_uip_master', 'Immunization::syncUipMaster', ['filter' => 'permission:doctor_work.immunization.schedule-manage']);
|
||||
|
|
@ -1084,6 +1097,10 @@ $routes->get('Report/report_total_payment_app_show/(:segment)/(:segment)/(:segme
|
|||
$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/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]);
|
||||
$routes->get('Report/old_payment_received_report_data/(:segment)/(:num)', 'Report::old_payment_received_report_data/$1/$2', ['filter' => $reportsCollectionFilter]);
|
||||
|
||||
$reportsIpdCensusFilter = 'permission:reports.ipd_census.view';
|
||||
$routes->get('Report/ipd_census_report', 'Report::ipd_census_report', ['filter' => $reportsIpdCensusFilter]);
|
||||
|
|
@ -1112,7 +1129,8 @@ $routes->get('Report/diagnosis_report', 'Report::diagnosis_report', ['filter' =>
|
|||
$routes->get('Report/diagnosis_report_data/(:segment)', 'Report::diagnosis_report_data/$1', ['filter' => 'permission:diagnosis.report.view,diagnosis.access']);
|
||||
$routes->get('Report/diagnosis_report_data/(:segment)/(:segment)', 'Report::diagnosis_report_data/$1/$2', ['filter' => 'permission:diagnosis.report.view,diagnosis.access']);
|
||||
$routes->get('Report/diagnosis_report_data/(:segment)/(:segment)/(:segment)', 'Report::diagnosis_report_data/$1/$2/$3', ['filter' => 'permission:diagnosis.report.view,diagnosis.access']);
|
||||
$routes->get('Report/diagnosis_report_data/(:segment)/(:segment)/(:segment)/(:num)', 'Report::diagnosis_report_data/$1/$2/$3/$4', ['filter' => 'permission:diagnosis.report.view,diagnosis.access']);
|
||||
$routes->get('Report/reconciliation_mismatch_report', 'Report::reconciliation_mismatch_report');
|
||||
$routes->get('Report/reconciliation_mismatch_report/(:segment)', 'Report::reconciliation_mismatch_report/$1');
|
||||
|
||||
$reportsNabhFilter = 'permission:reports.nabh_audit.view,reports.access';
|
||||
$routes->get('Report/nabh_audit_report', 'Report::nabh_audit_report', ['filter' => $reportsNabhFilter]);
|
||||
|
|
@ -1554,11 +1572,23 @@ $routes->group('Item_IPD', ['filter' => 'permission:billing.items.view,billing.i
|
|||
$routes->get('DoctorDocument', 'DoctorDocument::index');
|
||||
$routes->get('DoctorDocument/index', 'DoctorDocument::index');
|
||||
$routes->get('DoctorDocument/workspace', 'DoctorDocument::workspace');
|
||||
$routes->get('DoctorDocument/search_ipd_patient', 'DoctorDocument::search_ipd_patient');
|
||||
$routes->post('DoctorDocument/upload_ipd_nabh_document', 'DoctorDocument::upload_ipd_nabh_document');
|
||||
$routes->post('DoctorDocument/push_ipd_nabh_to_abdm', 'DoctorDocument::push_ipd_nabh_to_abdm');
|
||||
$routes->get('DoctorDocument/health_document_fhir_preview/(:num)', 'DoctorDocument::health_document_fhir_preview/$1');
|
||||
$routes->get('DoctorDocument/wellness_record_fhir_preview', 'DoctorDocument::wellness_record_fhir_preview');
|
||||
$routes->get('DoctorDocument/wellness_record_fhir_preview/(:num)', 'DoctorDocument::wellness_record_fhir_preview/$1');
|
||||
$routes->get('DoctorDocument/wellness_record_fhir_preview/(:num)/(:num)', 'DoctorDocument::wellness_record_fhir_preview/$1/$2');
|
||||
$routes->get('Document_Patient', 'DoctorDocument::index');
|
||||
$routes->get('Document_Patient/index', 'DoctorDocument::index');
|
||||
$routes->get('Document_Patient/workspace', 'DoctorDocument::workspace');
|
||||
$routes->get('Document_Patient/search_ipd_patient', 'DoctorDocument::search_ipd_patient');
|
||||
$routes->post('Document_Patient/upload_ipd_nabh_document', 'DoctorDocument::upload_ipd_nabh_document');
|
||||
$routes->post('Document_Patient/push_ipd_nabh_to_abdm', 'DoctorDocument::push_ipd_nabh_to_abdm');
|
||||
$routes->get('Document_Patient/health_document_fhir_preview/(:num)', 'DoctorDocument::health_document_fhir_preview/$1');
|
||||
$routes->get('Document_Patient/wellness_record_fhir_preview', 'DoctorDocument::wellness_record_fhir_preview');
|
||||
$routes->get('Document_Patient/wellness_record_fhir_preview/(:num)', 'DoctorDocument::wellness_record_fhir_preview/$1');
|
||||
$routes->get('Document_Patient/wellness_record_fhir_preview/(:num)/(:num)', 'DoctorDocument::wellness_record_fhir_preview/$1/$2');
|
||||
|
||||
/*
|
||||
* --------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -2395,10 +2395,24 @@ class AbdmGateway extends BaseController
|
|||
}
|
||||
|
||||
$patientId = (int) $this->request->getPost('patient_id');
|
||||
$opdId = (int) $this->request->getPost('opd_id');
|
||||
$opdId = (int) ($this->request->getPost('opd_id') ?? $this->request->getPost('record_id') ?? $this->request->getPost('entity_id') ?? 0);
|
||||
$taskId = (int) $this->request->getPost('task_id');
|
||||
$abhaId = trim((string) ($this->request->getPost('abha_id') ?? $this->request->getPost('abha_address') ?? ''));
|
||||
$consentHandle = trim((string) $this->request->getPost('consent_handle'));
|
||||
|
||||
if ($patientId <= 0 && $taskId > 0 && $this->db->tableExists('abdm_work_tasks')) {
|
||||
$t = $this->db->table('abdm_work_tasks')->where('id', $taskId)->get(1)->getRowArray();
|
||||
if (! empty($t)) {
|
||||
$patientId = (int) ($t['patient_id'] ?? 0);
|
||||
if ($opdId <= 0) {
|
||||
$opdId = (int) ($t['entity_id'] ?? 0);
|
||||
}
|
||||
if ($abhaId === '') {
|
||||
$abhaId = (string) ($t['abha_id'] ?? '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($patientId <= 0) {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'patient_id is required']);
|
||||
}
|
||||
|
|
@ -2407,6 +2421,9 @@ class AbdmGateway extends BaseController
|
|||
if ($payload === null) {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'No wellness/vital data found for this patient']);
|
||||
}
|
||||
if ($taskId > 0) {
|
||||
$payload['task_id'] = $taskId;
|
||||
}
|
||||
|
||||
if ($abhaId === '') {
|
||||
$abhaId = $this->resolvePatientAbhaIdentifier($patientId);
|
||||
|
|
@ -2422,16 +2439,34 @@ class AbdmGateway extends BaseController
|
|||
}
|
||||
|
||||
$patientId = (int) $this->request->getPost('patient_id');
|
||||
$recordId = (int) ($this->request->getPost('record_id') ?? $this->request->getPost('patient_doc_id') ?? $this->request->getPost('entity_id') ?? 0);
|
||||
$taskId = (int) $this->request->getPost('task_id');
|
||||
$abhaId = trim((string) ($this->request->getPost('abha_id') ?? $this->request->getPost('abha_address') ?? ''));
|
||||
$consentHandle = trim((string) $this->request->getPost('consent_handle'));
|
||||
|
||||
if ($patientId <= 0 && $taskId > 0 && $this->db->tableExists('abdm_work_tasks')) {
|
||||
$t = $this->db->table('abdm_work_tasks')->where('id', $taskId)->get(1)->getRowArray();
|
||||
if (! empty($t)) {
|
||||
$patientId = (int) ($t['patient_id'] ?? 0);
|
||||
if ($recordId <= 0) {
|
||||
$recordId = (int) ($t['entity_id'] ?? 0);
|
||||
}
|
||||
if ($abhaId === '') {
|
||||
$abhaId = (string) ($t['abha_id'] ?? '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($patientId <= 0) {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'patient_id is required']);
|
||||
}
|
||||
|
||||
$payload = $this->buildHealthDocumentRecordPayload($patientId, $abhaId);
|
||||
$payload = $this->buildHealthDocumentRecordPayload($patientId, $abhaId, $recordId);
|
||||
if ($payload === null) {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'document_title with document_text/document_base64/file_path is required']);
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'Health document file or metadata not found for record #' . $recordId]);
|
||||
}
|
||||
if ($taskId > 0) {
|
||||
$payload['task_id'] = $taskId;
|
||||
}
|
||||
|
||||
if ($abhaId === '') {
|
||||
|
|
@ -6287,6 +6322,29 @@ class AbdmGateway extends BaseController
|
|||
'error_message' => (string) ($connectorError ?? ''),
|
||||
]);
|
||||
|
||||
$taskId = (int) ($payload['task_id'] ?? 0);
|
||||
if ($this->db->tableExists('abdm_work_tasks')) {
|
||||
$taskStatus = $connectorError === null ? 'in_progress' : 'failed';
|
||||
$taskResult = $connectorError === null
|
||||
? 'Submitted to ABDM Bridge at ' . $ccRef
|
||||
. ($queueId ? '; queue ID ' . $queueId : '')
|
||||
. ($bridgeRecordId > 0 ? '; record ID ' . $bridgeRecordId : '') . '.'
|
||||
: 'ABDM Bridge push failed for ' . $ccRef . ': ' . $connectorError;
|
||||
|
||||
$builder = $this->db->table('abdm_work_tasks');
|
||||
if ($taskId > 0) {
|
||||
$builder->where('id', $taskId);
|
||||
} else {
|
||||
$builder->where('entity_id', (string) $entityId)
|
||||
->whereIn('status', ['pending', 'in_progress', 'failed']);
|
||||
}
|
||||
$builder->update([
|
||||
'status' => $taskStatus,
|
||||
'last_action_result' => $taskResult,
|
||||
'updated_at' => Time::now('Asia/Kolkata')->toDateTimeString(),
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->response->setJSON([
|
||||
'ok' => $connectorError === null ? 1 : 0,
|
||||
'queue_id' => $queueId,
|
||||
|
|
@ -6312,43 +6370,115 @@ class AbdmGateway extends BaseController
|
|||
if ($this->db->tableExists('opd_prescription')) {
|
||||
$builder = $this->db->table('opd_prescription')->where('p_id', $patientId);
|
||||
if ($opdId > 0) {
|
||||
$builder->where('opd_id', $opdId);
|
||||
$builder->groupStart()
|
||||
->where('id', $opdId)
|
||||
->orWhere('opd_id', $opdId)
|
||||
->groupEnd();
|
||||
}
|
||||
$builder->groupStart()
|
||||
->where("COALESCE(NULLIF(TRIM(bp), ''), NULLIF(TRIM(diastolic), ''), NULLIF(TRIM(pulse), ''), NULLIF(TRIM(height), ''), NULLIF(TRIM(weight), ''), NULLIF(TRIM(temp), ''), NULLIF(TRIM(rr_min), ''), NULLIF(TRIM(spo2), ''), NULLIF(TRIM(glucose), '')) IS NOT NULL", null, false)
|
||||
->groupEnd();
|
||||
$row = $builder->orderBy('id', 'DESC')->get(1)->getRowArray() ?? [];
|
||||
|
||||
if (empty($row) && $opdId > 0) {
|
||||
$row = $this->db->table('opd_prescription')->where('p_id', $patientId)->orderBy('id', 'DESC')->get(1)->getRowArray() ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
$vitals = $this->extractWellnessVitals($row);
|
||||
$lifestyle = $this->extractPatientLifestyleObservations($patientRow);
|
||||
if (empty($vitals) && empty($lifestyle)) {
|
||||
return null;
|
||||
|
||||
$bundle = [];
|
||||
$careContextRef = '';
|
||||
$careContextDisp = '';
|
||||
if (class_exists('\App\Controllers\DoctorDocument')) {
|
||||
try {
|
||||
$docController = new \App\Controllers\DoctorDocument();
|
||||
$source = $docController->buildWellnessRecordSource($patientId, $opdId);
|
||||
if (! empty($source)) {
|
||||
$factory = new FhirGeneratorFactory();
|
||||
$res = $factory->wellness()->generate($source);
|
||||
$hfrId = (string) ($source['hfr_id'] ?? 'HFR-IN-HMS');
|
||||
$adapter = new \App\Libraries\Abdm\Fhir\Support\GatewayPayloadAdapter();
|
||||
$gatewayPayload = $adapter->toGatewayPayload($res, $source, $hfrId);
|
||||
$bundle = (array) ($gatewayPayload['fhir_bundle'] ?? []);
|
||||
$careContextRef = (string) ($gatewayPayload['care_context_reference'] ?? '');
|
||||
$careContextDisp = (string) ($gatewayPayload['care_context_display'] ?? '');
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Fallback to simple wellness bundle
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($bundle)) {
|
||||
if (empty($vitals) && empty($lifestyle)) {
|
||||
return null;
|
||||
}
|
||||
$patient = $this->buildAbdmPatientResource($patientRow, $patientId, $abhaId);
|
||||
$bundle = $this->buildSimpleWellnessBundle($patient, $vitals, $lifestyle);
|
||||
}
|
||||
|
||||
$patient = $this->buildAbdmPatientResource($patientRow, $patientId, $abhaId);
|
||||
$bundle = $this->buildSimpleWellnessBundle($patient, $vitals, $lifestyle);
|
||||
$visitDate = (string) ($row['date_opd_visit'] ?? date('Y-m-d'));
|
||||
$entityId = (string) ($opdId > 0 ? $opdId : ($row['opd_id'] ?? $patientId));
|
||||
$entityId = (string) ($opdId > 0 ? $opdId : ($row['opd_id'] ?? $row['id'] ?? $patientId));
|
||||
if ($careContextRef === '') {
|
||||
$careContextRef = 'WELLNESS-' . $entityId . '-' . date('Y-m-d', strtotime($visitDate));
|
||||
}
|
||||
if ($careContextDisp === '') {
|
||||
$careContextDisp = 'Wellness Record - ' . date('d/m/Y', strtotime($visitDate));
|
||||
}
|
||||
|
||||
return [
|
||||
'hi_type' => 'WellnessRecord',
|
||||
'entity_type' => 'wellness',
|
||||
'entity_type' => 'opd_vitals',
|
||||
'entity_id' => $entityId,
|
||||
'patient_id' => $patientId,
|
||||
'patient_name' => $this->patientDisplayName($patientRow),
|
||||
'visit_date' => $visitDate,
|
||||
'care_context_reference' => 'WELLNESS-' . $entityId . '-' . date('Y-m-d', strtotime($visitDate)),
|
||||
'care_context_display' => 'Wellness Record - ' . date('d/m/Y', strtotime($visitDate)),
|
||||
'care_context_reference' => $careContextRef,
|
||||
'care_context_display' => $careContextDisp,
|
||||
'bundle' => $bundle,
|
||||
];
|
||||
}
|
||||
|
||||
private function buildHealthDocumentRecordPayload(int $patientId, string $abhaId): ?array
|
||||
private function buildHealthDocumentRecordPayload(int $patientId, string $abhaId = '', int $recordId = 0, array $patientRow = []): ?array
|
||||
{
|
||||
$patientRow = $this->loadPatientRow($patientId);
|
||||
if (empty($patientRow)) {
|
||||
return null;
|
||||
$patientRow = $this->loadPatientRow($patientId);
|
||||
if (empty($patientRow)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
$targetRecordId = $recordId > 0
|
||||
? $recordId
|
||||
: (int) ($this->request->getPost('record_id') ?? $this->request->getPost('patient_doc_id') ?? $this->request->getPost('document_id') ?? $this->request->getPost('entity_id') ?? 0);
|
||||
|
||||
if ($targetRecordId > 0 && class_exists('\App\Controllers\DoctorDocument')) {
|
||||
try {
|
||||
$docController = new \App\Controllers\DoctorDocument();
|
||||
$source = $docController->buildHealthDocumentSource($targetRecordId);
|
||||
if (! empty($source)) {
|
||||
$factory = new FhirGeneratorFactory();
|
||||
$output = $factory->healthDocument()->generate($source);
|
||||
$hfrId = (string) ($source['hfr_id'] ?? 'HFR-IN-HMS');
|
||||
$adapter = new \App\Libraries\Abdm\Fhir\Support\GatewayPayloadAdapter();
|
||||
$gatewayPayload = $adapter->toGatewayPayload($output, $source, $hfrId);
|
||||
|
||||
return [
|
||||
'hi_type' => 'HealthDocumentRecord',
|
||||
'entity_type' => 'patient_doc',
|
||||
'entity_id' => (string) $targetRecordId,
|
||||
'patient_id' => $patientId,
|
||||
'patient_name' => $this->patientDisplayName($patientRow),
|
||||
'visit_date' => (string) ($source['visit_date'] ?? date('Y-m-d')),
|
||||
'care_context_reference' => (string) ($gatewayPayload['care_context_reference'] ?? ('HDOC-' . $patientId . '-' . $targetRecordId)),
|
||||
'care_context_display' => (string) ($source['document_title'] ?? 'Health Document'),
|
||||
'bundle' => (array) ($gatewayPayload['fhir_bundle'] ?? []),
|
||||
];
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Fall through
|
||||
}
|
||||
}
|
||||
|
||||
$title = trim((string) ($this->request->getPost('document_title') ?? $this->request->getPost('title') ?? ''));
|
||||
|
|
@ -6371,12 +6501,14 @@ class AbdmGateway extends BaseController
|
|||
}
|
||||
}
|
||||
if ($base64 === '') {
|
||||
return null;
|
||||
$base64 = base64_encode('<div xmlns="http://www.w3.org/1999/xhtml"><h3>' . esc($title) . '</h3><p>Patient Health Document #' . ($targetRecordId > 0 ? $targetRecordId : $patientId) . '</p></div>');
|
||||
$contentType = 'text/html';
|
||||
}
|
||||
$contentType = $contentType !== '' ? $contentType : 'application/octet-stream';
|
||||
|
||||
$visitDate = date('Y-m-d');
|
||||
$entityId = trim((string) ($this->request->getPost('document_id') ?? $this->request->getPost('entity_id') ?? '')) ?: (string) $patientId;
|
||||
$entityId = $targetRecordId > 0 ? (string) $targetRecordId : (string) $patientId;
|
||||
|
||||
$patient = $this->buildAbdmPatientResource($patientRow, $patientId, $abhaId);
|
||||
$bundle = $this->buildSimpleHealthDocumentBundle($patient, $title, $contentType, $base64);
|
||||
|
||||
|
|
@ -6387,7 +6519,7 @@ class AbdmGateway extends BaseController
|
|||
'patient_id' => $patientId,
|
||||
'patient_name' => $this->patientDisplayName($patientRow),
|
||||
'visit_date' => $visitDate,
|
||||
'care_context_reference' => 'HDOC-' . $patientId . '-' . $entityId . '-' . $visitDate,
|
||||
'care_context_reference' => 'HDOC-' . $patientId . '-' . $entityId . '-' . $visitDate . '-' . date('His'),
|
||||
'care_context_display' => $title,
|
||||
'bundle' => $bundle,
|
||||
];
|
||||
|
|
@ -6851,20 +6983,193 @@ class AbdmGateway extends BaseController
|
|||
$hash = md5($value);
|
||||
return sprintf('%s-%s-4%s-a%s-%s', substr($hash, 0, 8), substr($hash, 8, 4), substr($hash, 13, 3), substr($hash, 17, 3), substr($hash, 20, 12));
|
||||
};
|
||||
|
||||
// Convert non-PDF content (e.g. text/html, image) into valid PDF binary
|
||||
$rawBytes = base64_decode($base64);
|
||||
if ($rawBytes === false || $rawBytes === '' || ! str_starts_with($rawBytes, '%PDF')) {
|
||||
try {
|
||||
$mpdfTempDir = defined('WRITEPATH') ? WRITEPATH . 'cache' . DIRECTORY_SEPARATOR . 'mpdf' : sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'mpdf';
|
||||
if (! is_dir($mpdfTempDir)) {
|
||||
@mkdir($mpdfTempDir, 0755, true);
|
||||
}
|
||||
ini_set('pcre.backtrack_limit', '5000000');
|
||||
$mpdf = new \Mpdf\Mpdf([
|
||||
'format' => 'A4',
|
||||
'margin_left' => 10,
|
||||
'margin_right' => 10,
|
||||
'margin_top' => 10,
|
||||
'margin_bottom' => 10,
|
||||
'tempDir' => $mpdfTempDir,
|
||||
]);
|
||||
|
||||
if (str_starts_with($contentType, 'image/')) {
|
||||
$ext = str_contains($contentType, 'png') ? 'png' : 'jpg';
|
||||
$tempImg = $mpdfTempDir . DIRECTORY_SEPARATOR . 'img_' . md5((string) $rawBytes) . '.' . $ext;
|
||||
@file_put_contents($tempImg, $rawBytes);
|
||||
$html = '<!DOCTYPE html><html><body style="margin:0;padding:0;text-align:center;">'
|
||||
. '<h3>' . esc($title) . '</h3>'
|
||||
. '<img src="' . $tempImg . '" style="max-width:100%; height:auto;" />'
|
||||
. '</body></html>';
|
||||
$mpdf->WriteHTML($html);
|
||||
@unlink($tempImg);
|
||||
} else {
|
||||
$htmlStr = (is_string($rawBytes) && str_contains($rawBytes, '<')) ? $rawBytes : ('<!DOCTYPE html><html><body><h3>' . esc($title) . '</h3><p>' . nl2br(esc((string) $rawBytes)) . '</p></body></html>');
|
||||
$mpdf->WriteHTML($htmlStr);
|
||||
}
|
||||
$pdfBytes = $mpdf->Output('', 'S');
|
||||
if (is_string($pdfBytes) && str_starts_with($pdfBytes, '%PDF')) {
|
||||
$rawBytes = $pdfBytes;
|
||||
$base64 = base64_encode($pdfBytes);
|
||||
$contentType = 'application/pdf';
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Fallback
|
||||
}
|
||||
} else {
|
||||
$contentType = 'application/pdf';
|
||||
}
|
||||
|
||||
$fileSize = is_string($rawBytes) ? strlen($rawBytes) : 0;
|
||||
$sha1Hash = (is_string($rawBytes) && $rawBytes !== '') ? base64_encode(sha1($rawBytes, true)) : '';
|
||||
|
||||
$patientUrl = 'urn:uuid:' . $uuid($seed . '|patient');
|
||||
$binaryUrl = 'urn:uuid:' . $uuid($seed . '|binary');
|
||||
$practitionerUrl = 'urn:uuid:' . $uuid($seed . '|practitioner');
|
||||
$organizationUrl = 'urn:uuid:' . $uuid($seed . '|organization');
|
||||
$docRefUrl = 'urn:uuid:' . $uuid($seed . '|docref');
|
||||
|
||||
$patient['meta']['profile'] = array_values(array_unique(array_merge((array) ($patient['meta']['profile'] ?? []), ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Patient'])));
|
||||
$hospital = $this->getHospitalProfileForFhir();
|
||||
$organization = ['resourceType' => 'Organization', 'id' => substr($organizationUrl, 9), 'name' => trim((string) ($hospital['name'] ?? '')) ?: 'Healthcare Facility'];
|
||||
$composition = ['resourceType' => 'Composition', 'id' => substr($uuid($seed . '|composition'), 0), 'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/HealthDocumentRecord']], 'status' => 'final', 'type' => ['text' => 'Health Document Record'], 'subject' => ['reference' => $patientUrl], 'author' => [['reference' => $organizationUrl]], 'date' => $issuedAt, 'title' => $title, 'section' => [['title' => 'Invoice PDF', 'entry' => [['reference' => $binaryUrl, 'type' => 'Binary']]]]];
|
||||
|
||||
$practitioner = [
|
||||
'resourceType' => 'Practitioner',
|
||||
'id' => substr($practitionerUrl, 9),
|
||||
'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Practitioner']],
|
||||
'identifier' => [[
|
||||
'system' => 'https://doctor.ndhm.gov.in',
|
||||
'value' => 'HPR-12345',
|
||||
]],
|
||||
'name' => [['text' => 'Dr. Medical Officer']],
|
||||
];
|
||||
|
||||
$organization = [
|
||||
'resourceType' => 'Organization',
|
||||
'id' => substr($organizationUrl, 9),
|
||||
'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Organization']],
|
||||
'name' => trim((string) ($hospital['name'] ?? '')) ?: 'E-Atria Hospital',
|
||||
'identifier' => [[
|
||||
'system' => 'https://facility.ndhm.gov.in',
|
||||
'value' => trim((string) ($hospital['hfr_id'] ?? 'IN0510000871')) ?: 'IN0510000871',
|
||||
]],
|
||||
];
|
||||
|
||||
$documentReference = [
|
||||
'resourceType' => 'DocumentReference',
|
||||
'id' => substr($docRefUrl, 9),
|
||||
'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/DocumentReference']],
|
||||
'status' => 'current',
|
||||
'docStatus' => 'final',
|
||||
'type' => [
|
||||
'coding' => [
|
||||
[
|
||||
'system' => 'http://snomed.info/sct',
|
||||
'code' => '419891008',
|
||||
'display' => 'Record artifact',
|
||||
],
|
||||
[
|
||||
'system' => 'http://loinc.org',
|
||||
'code' => '41988-7',
|
||||
'display' => 'Medical statement',
|
||||
],
|
||||
],
|
||||
'text' => $title,
|
||||
],
|
||||
'subject' => ['reference' => $patientUrl],
|
||||
'author' => [[
|
||||
'reference' => $practitionerUrl,
|
||||
'display' => 'Dr. Medical Officer',
|
||||
]],
|
||||
'custodian' => ['reference' => $organizationUrl],
|
||||
'date' => $issuedAt,
|
||||
'description' => $title . ' PDF',
|
||||
'content' => [[
|
||||
'attachment' => [
|
||||
'contentType' => 'application/pdf',
|
||||
'language' => 'en-IN',
|
||||
'data' => $base64,
|
||||
'size' => $fileSize,
|
||||
'title' => $title . ' PDF',
|
||||
'creation' => $issuedAt,
|
||||
],
|
||||
]],
|
||||
];
|
||||
|
||||
if ($sha1Hash !== '') {
|
||||
$documentReference['content'][0]['attachment']['hash'] = $sha1Hash;
|
||||
}
|
||||
|
||||
$composition = [
|
||||
'resourceType' => 'Composition',
|
||||
'id' => substr($uuid($seed . '|composition'), 9),
|
||||
'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/HealthDocumentRecord']],
|
||||
'status' => 'final',
|
||||
'type' => [
|
||||
'coding' => [
|
||||
[
|
||||
'system' => 'http://snomed.info/sct',
|
||||
'code' => '419891008',
|
||||
'display' => 'Record artifact',
|
||||
],
|
||||
[
|
||||
'system' => 'http://loinc.org',
|
||||
'code' => '41988-7',
|
||||
'display' => 'Medical statement',
|
||||
],
|
||||
],
|
||||
'text' => 'Health Document Record',
|
||||
],
|
||||
'subject' => ['reference' => $patientUrl],
|
||||
'author' => [[
|
||||
'reference' => $practitionerUrl,
|
||||
'display' => 'Dr. Medical Officer',
|
||||
]],
|
||||
'custodian' => ['reference' => $organizationUrl],
|
||||
'date' => $issuedAt,
|
||||
'title' => $title,
|
||||
'section' => [[
|
||||
'title' => $title,
|
||||
'code' => [
|
||||
'coding' => [
|
||||
[
|
||||
'system' => 'http://snomed.info/sct',
|
||||
'code' => '419891008',
|
||||
'display' => 'Record artifact',
|
||||
],
|
||||
[
|
||||
'system' => 'http://loinc.org',
|
||||
'code' => '41988-7',
|
||||
'display' => 'Medical statement',
|
||||
],
|
||||
],
|
||||
],
|
||||
'entry' => [['reference' => $docRefUrl]],
|
||||
]],
|
||||
];
|
||||
|
||||
$entries = [
|
||||
['fullUrl' => 'urn:uuid:' . $uuid($seed . '|composition'), 'resource' => $composition],
|
||||
['fullUrl' => $patientUrl, 'resource' => $patient],
|
||||
['fullUrl' => $practitionerUrl, 'resource' => $practitioner],
|
||||
['fullUrl' => $organizationUrl, 'resource' => $organization],
|
||||
['fullUrl' => $docRefUrl, 'resource' => $documentReference],
|
||||
];
|
||||
|
||||
return [
|
||||
'resourceType' => 'Bundle',
|
||||
'type' => 'document',
|
||||
'timestamp' => $issuedAt,
|
||||
'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/DocumentBundle']],
|
||||
'entry' => $entries,
|
||||
];
|
||||
$entries[] = ['fullUrl' => $binaryUrl, 'resource' => ['resourceType' => 'Binary', 'id' => substr($binaryUrl, 9), 'contentType' => $contentType, 'data' => $base64]];
|
||||
return ['resourceType' => 'Bundle', 'type' => 'document', 'timestamp' => $issuedAt, 'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/DocumentBundle']], 'entry' => $entries];
|
||||
}
|
||||
|
||||
private function buildSimpleInvoiceBundle(array $patient, array $invoice, array $items): array
|
||||
|
|
@ -7334,20 +7639,31 @@ class AbdmGateway extends BaseController
|
|||
|
||||
private function resolveReadableAttachmentPath(string $path): string
|
||||
{
|
||||
$candidate = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $path);
|
||||
if (! str_starts_with($candidate, DIRECTORY_SEPARATOR) && ! preg_match('/^[A-Za-z]:\\\\/', $candidate)) {
|
||||
$candidate = FCPATH . ltrim($candidate, DIRECTORY_SEPARATOR);
|
||||
}
|
||||
$real = realpath($candidate);
|
||||
if ($real === false || ! is_file($real) || ! is_readable($real)) {
|
||||
if ($path === '') {
|
||||
return '';
|
||||
}
|
||||
$allowed = array_filter([realpath(FCPATH), realpath(WRITEPATH)]);
|
||||
foreach ($allowed as $root) {
|
||||
if (str_starts_with($real, (string) $root)) {
|
||||
return $real;
|
||||
|
||||
$candidate = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $path);
|
||||
$candidates = [$candidate];
|
||||
if (! str_starts_with($candidate, DIRECTORY_SEPARATOR) && ! preg_match('/^[A-Za-z]:\\\\/', $candidate)) {
|
||||
$candidates[] = FCPATH . ltrim($candidate, DIRECTORY_SEPARATOR);
|
||||
$candidates[] = FCPATH . 'uploads' . DIRECTORY_SEPARATOR . ltrim($candidate, DIRECTORY_SEPARATOR);
|
||||
}
|
||||
|
||||
$fileName = basename($path);
|
||||
if ($fileName !== '' && defined('FCPATH')) {
|
||||
$matches = glob(rtrim(FCPATH, '/\\') . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . '*' . DIRECTORY_SEPARATOR . $fileName);
|
||||
if (! empty($matches)) {
|
||||
$candidates[] = $matches[0];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($candidates as $c) {
|
||||
if ($c !== '' && is_file($c) && is_readable($c)) {
|
||||
return realpath($c) ?: $c;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ class AbdmTaskBoard extends BaseController
|
|||
$this->backfillLabRadiologyTasks();
|
||||
$this->backfillImmunizationTasks();
|
||||
$this->backfillHealthDocumentTasks();
|
||||
$this->backfillWellnessTasks();
|
||||
$tasks = $this->enrichImmunizationPushState($this->taskService->getOpenTasks(300));
|
||||
|
||||
$dateFrom = trim((string) ($this->request->getGet('date_from') ?? date('Y-m-d')));
|
||||
|
|
@ -52,6 +53,7 @@ class AbdmTaskBoard extends BaseController
|
|||
$this->backfillLabRadiologyTasks();
|
||||
$this->backfillImmunizationTasks();
|
||||
$this->backfillHealthDocumentTasks();
|
||||
$this->backfillWellnessTasks();
|
||||
return $this->response->setJSON([
|
||||
'ok' => 1,
|
||||
'tasks' => $this->enrichImmunizationPushState($this->taskService->getOpenTasks(300)),
|
||||
|
|
@ -154,11 +156,51 @@ class AbdmTaskBoard extends BaseController
|
|||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'ABHA ID must be a 14-digit number']);
|
||||
}
|
||||
|
||||
$taskType = (string) ($task['task_type'] ?? '');
|
||||
$patientId = (int) ($task['patient_id'] ?? 0);
|
||||
$entityId = (int) ($task['entity_id'] ?? 0);
|
||||
|
||||
if (strtolower($action) === 'submit') {
|
||||
$gateway = new AbdmGateway();
|
||||
$gateway->initController($this->request, $this->response, service('logger'));
|
||||
|
||||
$_POST['task_id'] = $taskId;
|
||||
$_POST['patient_id'] = $patientId;
|
||||
$_POST['abha_id'] = $abhaId;
|
||||
$_POST['push_to_gateway'] = 1;
|
||||
|
||||
if ($taskType === 'wellness_record_publish') {
|
||||
$_POST['opd_id'] = $entityId;
|
||||
return $gateway->shareWellnessBundle();
|
||||
}
|
||||
if ($taskType === 'health_document_publish') {
|
||||
$_POST['record_id'] = $entityId;
|
||||
$_POST['patient_doc_id'] = $entityId;
|
||||
return $gateway->shareHealthDocumentBundle();
|
||||
}
|
||||
if ($taskType === 'immunization_record_publish') {
|
||||
$_POST['record_id'] = $entityId;
|
||||
return $gateway->shareImmunizationBundle();
|
||||
}
|
||||
if ($taskType === 'opd_prescription_publish') {
|
||||
$_POST['opd_id'] = $entityId;
|
||||
return $gateway->sharePrescriptionBundle();
|
||||
}
|
||||
if ($taskType === 'lab_report_publish' || $taskType === 'radiology_report_publish') {
|
||||
$_POST['lab_req_id'] = $entityId;
|
||||
return $gateway->shareDiagnosisReportBundle();
|
||||
}
|
||||
if ($taskType === 'ipd_discharge_publish') {
|
||||
$_POST['ipd_id'] = $entityId;
|
||||
return $gateway->shareIpdDischargeBundle();
|
||||
}
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'task_id' => $taskId,
|
||||
'task_code' => (string) ($task['task_code'] ?? ''),
|
||||
'task_type' => (string) ($task['task_type'] ?? ''),
|
||||
'patient_id' => (int) ($task['patient_id'] ?? 0),
|
||||
'task_type' => $taskType,
|
||||
'patient_id' => $patientId,
|
||||
'patient_name' => (string) ($task['patient_name'] ?? ''),
|
||||
'abha_id' => $abhaId,
|
||||
'entity_type' => (string) ($task['entity_type'] ?? ''),
|
||||
|
|
@ -166,7 +208,7 @@ class AbdmTaskBoard extends BaseController
|
|||
'opd_session_id' => (int) $this->request->getPost('opd_session_id'),
|
||||
];
|
||||
|
||||
$eventType = $this->resolveActionEventType($action, (string) ($task['task_type'] ?? ''));
|
||||
$eventType = $this->resolveActionEventType($action, $taskType);
|
||||
if ($eventType === '') {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'Unsupported action']);
|
||||
}
|
||||
|
|
@ -222,6 +264,12 @@ class AbdmTaskBoard extends BaseController
|
|||
if ($taskType === 'health_document_publish') {
|
||||
return 'abdm.health_document.share.requested';
|
||||
}
|
||||
if ($taskType === 'wellness_record_publish') {
|
||||
return 'abdm.wellness_record.share.requested';
|
||||
}
|
||||
if ($taskType === 'immunization_record_publish') {
|
||||
return 'abdm.immunization_record.share.requested';
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
|
|
@ -1079,5 +1127,106 @@ class AbdmTaskBoard extends BaseController
|
|||
]
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->db->tableExists('file_upload_data')) {
|
||||
$fileRows = $this->db->table('file_upload_data f')
|
||||
->select('f.id, f.pid, f.insert_date, p.p_fname, p.' . $abhaCol . ' as abha_id', false)
|
||||
->join('patient_master p', 'p.id = f.pid', 'inner')
|
||||
->where('p.' . $abhaCol . ' !=', '')
|
||||
->where('f.insert_date >=', date('Y-m-d H:i:s', strtotime('-30 days')))
|
||||
->orderBy('f.id', 'DESC')
|
||||
->limit(200)
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
foreach ($fileRows as $fRow) {
|
||||
$patientId = (int) ($fRow['pid'] ?? 0);
|
||||
$fileId = (int) ($fRow['id'] ?? 0);
|
||||
$abhaId = trim((string) ($fRow['abha_id'] ?? ''));
|
||||
if ($patientId <= 0 || $fileId <= 0 || preg_match('/^\d{14}$/', $abhaId) !== 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$patientName = trim((string) ($fRow['p_fname'] ?? ''));
|
||||
$this->taskService->createOrRefreshTask(
|
||||
'health_document_publish',
|
||||
'file_upload_data',
|
||||
'patient_document',
|
||||
(string) $fileId,
|
||||
$patientId,
|
||||
$patientName,
|
||||
$abhaId,
|
||||
'submit',
|
||||
[
|
||||
'file_upload_id' => $fileId,
|
||||
'trigger' => 'file_upload_data.created',
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function backfillWellnessTasks(): void
|
||||
{
|
||||
if (! $this->db->tableExists('opd_prescription') || ! $this->db->tableExists('patient_master')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$patientFields = $this->db->getFieldNames('patient_master') ?? [];
|
||||
$abhaCol = $this->resolveExistingColumn($patientFields, ['abha_id', 'abha_no', 'abha_address', 'abha']);
|
||||
if ($abhaCol === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$opdPrescFields = $this->db->getFieldNames('opd_prescription') ?? [];
|
||||
$vitalCols = array_values(array_intersect(['bp', 'diastolic', 'pulse', 'height', 'weight', 'temp', 'rr_min', 'spo2', 'glucose'], $opdPrescFields));
|
||||
if ($vitalCols === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$coalesceExpr = [];
|
||||
foreach ($vitalCols as $vc) {
|
||||
$coalesceExpr[] = "NULLIF(TRIM(pr." . $vc . "), '')";
|
||||
}
|
||||
$whereSql = "COALESCE(" . implode(', ', $coalesceExpr) . ") IS NOT NULL";
|
||||
|
||||
$dateCol = in_array('date_opd_visit', $opdPrescFields, true) ? 'pr.date_opd_visit' : 'pr.id';
|
||||
|
||||
$rows = $this->db->table('opd_prescription pr')
|
||||
->select('pr.id, pr.p_id, pr.opd_id, ' . $dateCol . ', p.p_fname, p.' . $abhaCol . ' as abha_id', false)
|
||||
->join('patient_master p', 'p.id = pr.p_id', 'inner')
|
||||
->where('p.' . $abhaCol . ' !=', '')
|
||||
->where($whereSql, null, false)
|
||||
->orderBy('pr.id', 'DESC')
|
||||
->limit(200)
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$patientId = (int) ($row['p_id'] ?? 0);
|
||||
$rxId = (int) ($row['id'] ?? 0);
|
||||
$opdId = (int) ($row['opd_id'] ?? 0);
|
||||
$abhaId = trim((string) ($row['abha_id'] ?? ''));
|
||||
if ($patientId <= 0 || $rxId <= 0 || preg_match('/^\d{14}$/', $abhaId) !== 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$patientName = trim((string) ($row['p_fname'] ?? ''));
|
||||
$this->taskService->createOrRefreshTask(
|
||||
'wellness_record_publish',
|
||||
'opd_prescription',
|
||||
'opd_vitals',
|
||||
(string) $rxId,
|
||||
$patientId,
|
||||
$patientName,
|
||||
$abhaId,
|
||||
'submit',
|
||||
[
|
||||
'opd_prescription_id' => $rxId,
|
||||
'opd_session_id' => $opdId,
|
||||
'trigger' => 'vitals.backfilled',
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -226,6 +226,47 @@ class DoctorDocument extends BaseController
|
|||
return 'data:' . $mime . ';base64,' . base64_encode($bytes);
|
||||
}
|
||||
|
||||
private function resolveHospitalLogoDataUri(): string
|
||||
{
|
||||
$logoFile = trim($this->getHospitalSettingValue('H_logo'));
|
||||
if ($logoFile === '' && defined('H_logo')) {
|
||||
$logoFile = (string) constant('H_logo');
|
||||
}
|
||||
|
||||
$logoAbsPath = '';
|
||||
if ($logoFile !== '') {
|
||||
$candidates = [
|
||||
FCPATH . 'assets/images/' . $logoFile,
|
||||
FCPATH . 'assets/images/' . ltrim($logoFile, '/\\'),
|
||||
FCPATH . $logoFile,
|
||||
];
|
||||
foreach ($candidates as $cand) {
|
||||
if (is_file($cand)) {
|
||||
$logoAbsPath = $cand;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($logoAbsPath === '') {
|
||||
$globMatches = glob(FCPATH . 'assets/images/hospital_logo_*');
|
||||
if (is_array($globMatches) && ! empty($globMatches)) {
|
||||
usort($globMatches, fn($a, $b) => filemtime($b) <=> filemtime($a));
|
||||
$logoAbsPath = $globMatches[0];
|
||||
}
|
||||
}
|
||||
|
||||
if ($logoAbsPath !== '') {
|
||||
$src = $this->buildImageDataUriFromPath($logoAbsPath);
|
||||
if ($src !== '') {
|
||||
return $src;
|
||||
}
|
||||
return str_replace('\\', '/', $logoAbsPath);
|
||||
}
|
||||
|
||||
return base_url('assets/images/' . ($logoFile !== '' ? $logoFile : 'logo.png'));
|
||||
}
|
||||
|
||||
private function hasColumn(string $table, string $column): bool
|
||||
{
|
||||
if (! $this->db->tableExists($table)) {
|
||||
|
|
@ -273,19 +314,515 @@ class DoctorDocument extends BaseController
|
|||
public function health_document_fhir_preview(int $patientDocId = 0)
|
||||
{
|
||||
if ($patientDocId <= 0) {
|
||||
$patientDocId = (int) ($this->request->getGet('patient_doc_id') ?? $this->request->getGet('doc_id') ?? 0);
|
||||
$patientDocId = (int) ($this->request->getGet('patient_doc_id') ?? $this->request->getGet('doc_id') ?? $this->request->getGet('health_doc_id') ?? 0);
|
||||
}
|
||||
|
||||
if ($patientDocId <= 0) {
|
||||
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'patient_doc_id is required']);
|
||||
}
|
||||
|
||||
$source = $this->buildHealthDocumentSource($patientDocId);
|
||||
if (empty($source)) {
|
||||
return $this->response->setStatusCode(404)->setJSON(['ok' => 0, 'error_text' => 'Health document source data not found for ID: ' . $patientDocId]);
|
||||
}
|
||||
|
||||
$factory = new \App\Libraries\Abdm\Fhir\FhirGeneratorFactory();
|
||||
$res = $factory->healthDocument()->generate([
|
||||
'patient_doc_id' => $patientDocId,
|
||||
$res = $factory->healthDocument()->generate($source);
|
||||
|
||||
$bundle = $res['fhir_bundle'] ?? $res['bundle'] ?? null;
|
||||
|
||||
return $this->response->setJSON([
|
||||
'status' => 'ok',
|
||||
'ok' => 1,
|
||||
'bundle' => $bundle,
|
||||
'fhir_bundle' => $bundle,
|
||||
'hi_type' => 'HealthDocumentRecord',
|
||||
'care_context_reference' => $res['care_context_reference'] ?? '',
|
||||
'care_context_display' => $res['care_context_display'] ?? '',
|
||||
'validation' => $res['validation'] ?? [],
|
||||
]);
|
||||
}
|
||||
|
||||
private function ensureNabhIpdScansTable(): bool
|
||||
{
|
||||
if (! $this->db->tableExists('nabh_ipd_scans')) {
|
||||
$sql = "CREATE TABLE IF NOT EXISTS `nabh_ipd_scans` (
|
||||
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
`ipd_id` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`patient_id` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`nabh_category` VARCHAR(50) NOT NULL,
|
||||
`category_title` VARCHAR(150) NOT NULL,
|
||||
`file_name` VARCHAR(255) NOT NULL,
|
||||
`file_path` VARCHAR(255) NOT NULL,
|
||||
`file_type` VARCHAR(100) NOT NULL,
|
||||
`file_size` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`patient_doc_id` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`abdm_status` VARCHAR(50) NOT NULL DEFAULT 'pending',
|
||||
`care_context_reference` VARCHAR(100) NULL,
|
||||
`uploaded_by` INT UNSIGNED NOT NULL DEFAULT 1,
|
||||
`created_at` DATETIME NULL,
|
||||
`updated_at` DATETIME NULL,
|
||||
KEY `idx_ipd_id` (`ipd_id`),
|
||||
KEY `idx_patient_id` (`patient_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;";
|
||||
try {
|
||||
$this->db->query($sql);
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'Unable to create nabh_ipd_scans table: {msg}', ['msg' => $e->getMessage()]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function search_ipd_patient()
|
||||
{
|
||||
if (! $this->request->isAJAX()) {
|
||||
return $this->response->setStatusCode(403)->setJSON(['status' => 'error', 'message' => 'AJAX only']);
|
||||
}
|
||||
|
||||
$query = trim((string) ($this->request->getGet('ipd_key') ?? $this->request->getGet('query') ?? ''));
|
||||
if ($query === '') {
|
||||
return $this->response->setStatusCode(400)->setJSON(['status' => 'error', 'message' => 'IPD No. or Patient identifier is required']);
|
||||
}
|
||||
|
||||
$builder = $this->db->table('ipd_master')
|
||||
->select('ipd_master.*, patient_master.p_fname, patient_master.p_lname, patient_master.p_code, patient_master.age, patient_master.gender, patient_master.dob, patient_master.abha_id, patient_master.abha_address, patient_master.mphone1, doctor_master.p_fname as dr_fname, doctor_master.p_lname as dr_lname')
|
||||
->join('patient_master', 'patient_master.id = ipd_master.p_id', 'left')
|
||||
->join('doctor_master', 'doctor_master.id = ipd_master.r_doc_id', 'left');
|
||||
|
||||
if (is_numeric($query)) {
|
||||
$builder->groupStart()
|
||||
->where('ipd_master.id', (int) $query)
|
||||
->orWhere('ipd_master.p_id', (int) $query)
|
||||
->orWhere('ipd_master.ipd_code', $query)
|
||||
->orWhere('patient_master.p_code', $query)
|
||||
->groupEnd();
|
||||
} else {
|
||||
$builder->groupStart()
|
||||
->like('ipd_master.ipd_code', $query)
|
||||
->orLike('ipd_master.P_name', $query)
|
||||
->orLike('patient_master.p_code', $query)
|
||||
->orLike('patient_master.p_fname', $query)
|
||||
->orLike('patient_master.p_lname', $query)
|
||||
->groupEnd();
|
||||
}
|
||||
|
||||
$ipdRecords = $builder->orderBy('ipd_master.id', 'DESC')->limit(10)->get()->getResultArray();
|
||||
|
||||
if (empty($ipdRecords)) {
|
||||
return $this->response->setStatusCode(404)->setJSON(['status' => 'error', 'message' => 'No IPD admission found matching "' . esc($query) . '"']);
|
||||
}
|
||||
|
||||
$this->ensureNabhIpdScansTable();
|
||||
|
||||
$outputList = [];
|
||||
foreach ($ipdRecords as $row) {
|
||||
$ipdId = (int) $row['id'];
|
||||
$patientId = (int) $row['p_id'];
|
||||
$patientName = trim(trim((string) ($row['p_fname'] ?? '')) . ' ' . trim((string) ($row['p_lname'] ?? '')));
|
||||
if ($patientName === '') {
|
||||
$patientName = (string) ($row['P_name'] ?? ('Patient #' . $patientId));
|
||||
}
|
||||
$doctorName = trim(trim((string) ($row['dr_fname'] ?? '')) . ' ' . trim((string) ($row['dr_lname'] ?? '')));
|
||||
if ($doctorName === '') {
|
||||
$doctorName = (string) ($row['r_doc_name'] ?? 'Attending Doctor');
|
||||
}
|
||||
|
||||
$scans = $this->db->table('nabh_ipd_scans')
|
||||
->where('ipd_id', $ipdId)
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
$scansByCategory = [];
|
||||
foreach ($scans as $scan) {
|
||||
$scansByCategory[$scan['nabh_category']] = [
|
||||
'scan_id' => (int) $scan['id'],
|
||||
'file_name' => $scan['file_name'],
|
||||
'file_url' => base_url('uploads/nabh_ipd/' . $scan['file_name']),
|
||||
'file_type' => $scan['file_type'],
|
||||
'file_size' => (int) $scan['file_size'],
|
||||
'patient_doc_id' => (int) $scan['patient_doc_id'],
|
||||
'abdm_status' => $scan['abdm_status'],
|
||||
'care_context_reference' => $scan['care_context_reference'],
|
||||
'uploaded_at' => $scan['created_at'],
|
||||
];
|
||||
}
|
||||
|
||||
$admDate = ! empty($row['register_date']) ? date('d-m-Y', strtotime((string) $row['register_date'])) : (! empty($row['insert_date']) ? date('d-m-Y', strtotime((string) $row['insert_date'])) : date('d-m-Y'));
|
||||
$disDate = ! empty($row['discharge_date']) ? date('d-m-Y', strtotime((string) $row['discharge_date'])) : null;
|
||||
|
||||
$outputList[] = [
|
||||
'ipd_id' => $ipdId,
|
||||
'ipd_no' => (string) (($row['ipd_code'] ?? '') !== '' ? $row['ipd_code'] : ('IPD-' . $ipdId)),
|
||||
'patient_id' => $patientId,
|
||||
'patient_name' => $patientName,
|
||||
'uhid' => (string) ($row['p_code'] ?? $patientId),
|
||||
'age' => (string) ($row['age'] ?? ''),
|
||||
'gender' => (string) ($row['gender'] ?? ''),
|
||||
'dob' => (string) ($row['dob'] ?? ''),
|
||||
'abha_id' => (string) ($row['abha_id'] ?? ''),
|
||||
'abha_address' => (string) ($row['abha_address'] ?? ''),
|
||||
'mobile' => (string) ($row['mphone1'] ?? $row['P_mobile1'] ?? ''),
|
||||
'doctor_name' => $doctorName,
|
||||
'doctor_id' => (int) ($row['r_doc_id'] ?? 1),
|
||||
'admission_date' => $admDate,
|
||||
'discharge_date' => $disDate,
|
||||
'scans' => $scansByCategory,
|
||||
];
|
||||
}
|
||||
|
||||
return $this->response->setJSON([
|
||||
'status' => 'success',
|
||||
'records' => $outputList,
|
||||
]);
|
||||
}
|
||||
|
||||
public function upload_ipd_nabh_document()
|
||||
{
|
||||
if (! $this->request->isAJAX()) {
|
||||
return $this->response->setStatusCode(403)->setJSON(['status' => 'error', 'message' => 'AJAX only']);
|
||||
}
|
||||
|
||||
$ipdId = (int) $this->request->getPost('ipd_id');
|
||||
$patientId = (int) $this->request->getPost('patient_id');
|
||||
$nabhCategory = trim((string) $this->request->getPost('nabh_category'));
|
||||
$categoryTitle = trim((string) $this->request->getPost('category_title'));
|
||||
|
||||
if ($ipdId <= 0 || $patientId <= 0 || $nabhCategory === '') {
|
||||
return $this->response->setStatusCode(400)->setJSON(['status' => 'error', 'message' => 'ipd_id, patient_id, and nabh_category are required']);
|
||||
}
|
||||
|
||||
$file = $this->request->getFile('scanned_file');
|
||||
if (! $file || ! $file->isValid()) {
|
||||
return $this->response->setStatusCode(400)->setJSON(['status' => 'error', 'message' => 'Valid scanned file (JPG, PNG, PDF) is required']);
|
||||
}
|
||||
|
||||
$origName = $file->getClientName();
|
||||
$ext = strtolower(pathinfo($origName, PATHINFO_EXTENSION) ?: 'pdf');
|
||||
$mimeType = $file->getClientMimeType() ?: 'application/pdf';
|
||||
|
||||
$uploadDir = FCPATH . 'uploads' . DIRECTORY_SEPARATOR . 'nabh_ipd' . DIRECTORY_SEPARATOR;
|
||||
if (! is_dir($uploadDir)) {
|
||||
@mkdir($uploadDir, 0755, true);
|
||||
}
|
||||
|
||||
$newFileName = 'NABH_IPD_' . $ipdId . '_' . str_replace('-', '_', $nabhCategory) . '_' . date('Ymd_His') . '.' . $ext;
|
||||
$file->move($uploadDir, $newFileName);
|
||||
|
||||
$fileFullPath = $uploadDir . $newFileName;
|
||||
$pdfPath = $fileFullPath;
|
||||
|
||||
if (function_exists('mime_content_type') && is_file($fileFullPath)) {
|
||||
$detected = @mime_content_type($fileFullPath);
|
||||
if (is_string($detected) && $detected !== '') {
|
||||
$mimeType = $detected;
|
||||
}
|
||||
}
|
||||
|
||||
if (in_array($ext, ['jpg', 'jpeg', 'png', 'webp'], true) && is_file($fileFullPath)) {
|
||||
try {
|
||||
$imgBytes = @file_get_contents($fileFullPath);
|
||||
if ($imgBytes !== false && $imgBytes !== '') {
|
||||
$base64Img = 'data:' . $mimeType . ';base64,' . base64_encode($imgBytes);
|
||||
$mpdf = new \Mpdf\Mpdf(['mode' => 'utf-8', 'format' => 'A4', 'tempDir' => WRITEPATH . 'cache']);
|
||||
$mpdf->WriteHTML('<div style="text-align:center;"><h3 style="font-family:sans-serif;">' . htmlspecialchars($categoryTitle) . '</h3><img src="' . $base64Img . '" style="max-width:100%;height:auto;" /></div>');
|
||||
$pdfFileName = pathinfo($newFileName, PATHINFO_FILENAME) . '.pdf';
|
||||
$pdfPath = $uploadDir . $pdfFileName;
|
||||
$mpdf->Output($pdfPath, \Mpdf\Output\Destination::FILE);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
log_message('warning', 'Unable to wrap image to PDF: {msg}', ['msg' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
$docId = $this->db->table('patient_doc')->insert([
|
||||
'p_id' => $patientId,
|
||||
'doc_format_id' => 1,
|
||||
'dr_id' => (int) ($this->request->getPost('doctor_id') ?? 1),
|
||||
'date_issue' => date('Y-m-d'),
|
||||
'raw_data' => json_encode([
|
||||
'title' => $categoryTitle,
|
||||
'nabh_category' => $nabhCategory,
|
||||
'ipd_id' => $ipdId,
|
||||
'file_name' => basename($pdfPath),
|
||||
'scanned_at' => date('Y-m-d H:i:s'),
|
||||
]),
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
|
||||
return $this->response->setJSON($res['bundle'] ?? $res);
|
||||
$patientDocId = (int) $this->db->insertID();
|
||||
|
||||
$this->enqueueHealthDocumentFhirSync($patientDocId);
|
||||
|
||||
$this->db->table('nabh_ipd_scans')
|
||||
->where('ipd_id', $ipdId)
|
||||
->where('nabh_category', $nabhCategory)
|
||||
->delete();
|
||||
|
||||
$user = auth()->user();
|
||||
$this->db->table('nabh_ipd_scans')->insert([
|
||||
'ipd_id' => $ipdId,
|
||||
'patient_id' => $patientId,
|
||||
'nabh_category' => $nabhCategory,
|
||||
'category_title' => $categoryTitle,
|
||||
'file_name' => basename($pdfPath),
|
||||
'file_path' => 'uploads/nabh_ipd/' . basename($pdfPath),
|
||||
'file_type' => $mimeType,
|
||||
'file_size' => (int) filesize($pdfPath),
|
||||
'patient_doc_id' => $patientDocId,
|
||||
'abdm_status' => 'scanned',
|
||||
'care_context_reference' => 'DOC-' . $patientDocId,
|
||||
'uploaded_by' => $user ? (int) $user->id : 1,
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
|
||||
return $this->response->setJSON([
|
||||
'status' => 'success',
|
||||
'message' => 'Scanned document uploaded successfully for ' . $categoryTitle,
|
||||
'patient_doc_id' => $patientDocId,
|
||||
'file_name' => basename($pdfPath),
|
||||
'file_url' => base_url('uploads/nabh_ipd/' . basename($pdfPath)),
|
||||
'nabh_category' => $nabhCategory,
|
||||
]);
|
||||
}
|
||||
|
||||
public function push_ipd_nabh_to_abdm()
|
||||
{
|
||||
if (! $this->request->isAJAX()) {
|
||||
return $this->response->setStatusCode(403)->setJSON(['status' => 'error', 'message' => 'AJAX only']);
|
||||
}
|
||||
|
||||
$patientDocId = (int) $this->request->getPost('patient_doc_id');
|
||||
|
||||
if ($patientDocId <= 0) {
|
||||
return $this->response->setStatusCode(400)->setJSON(['status' => 'error', 'message' => 'patient_doc_id is required']);
|
||||
}
|
||||
|
||||
$source = $this->buildHealthDocumentSource($patientDocId);
|
||||
if (empty($source)) {
|
||||
return $this->response->setStatusCode(404)->setJSON(['status' => 'error', 'message' => 'Source health document not found']);
|
||||
}
|
||||
|
||||
$factory = new \App\Libraries\Abdm\Fhir\FhirGeneratorFactory();
|
||||
$generatorOutput = $factory->healthDocument()->generate($source);
|
||||
|
||||
$hfrId = (string) ($source['hfr_id'] ?? 'HFR-IN-HMS');
|
||||
$adapter = new \App\Libraries\Abdm\Fhir\Support\GatewayPayloadAdapter();
|
||||
$payload = $adapter->toGatewayPayload($generatorOutput, $source, $hfrId);
|
||||
|
||||
$patientId = (int) ($source['patient']['id'] ?? 0);
|
||||
$abhaId = (string) ($source['patient']['abha_address'] ?? $source['patient']['abha_id'] ?? '');
|
||||
|
||||
$gatewayController = new \App\Controllers\AbdmGateway();
|
||||
$pushResponse = $gatewayController->pushAdditionalHiRecord($payload, $patientId, $abhaId);
|
||||
|
||||
$responseArray = json_decode((string) $pushResponse->getBody(), true) ?? [];
|
||||
|
||||
if (! empty($responseArray['ok']) || (! empty($responseArray['status']) && $responseArray['status'] === 'queued')) {
|
||||
$this->db->table('nabh_ipd_scans')
|
||||
->where('patient_doc_id', $patientDocId)
|
||||
->update([
|
||||
'abdm_status' => 'pushed',
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
}
|
||||
|
||||
return $pushResponse;
|
||||
}
|
||||
|
||||
public function wellness_record_fhir_preview(int $patientId = 0, int $opdSessionId = 0)
|
||||
{
|
||||
if ($patientId <= 0) {
|
||||
$patientId = (int) ($this->request->getGet('patient_id') ?? $this->request->getGet('p_id') ?? $this->request->getGet('pid') ?? 0);
|
||||
}
|
||||
if ($opdSessionId <= 0) {
|
||||
$opdSessionId = (int) ($this->request->getGet('opd_id') ?? $this->request->getGet('session_id') ?? 0);
|
||||
}
|
||||
|
||||
if ($patientId <= 0) {
|
||||
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'patient_id is required']);
|
||||
}
|
||||
|
||||
$source = $this->buildWellnessRecordSource($patientId, $opdSessionId);
|
||||
if (empty($source)) {
|
||||
return $this->response->setStatusCode(404)->setJSON(['ok' => 0, 'error_text' => 'Wellness source data (vitals/prescription) not found for patient #' . $patientId]);
|
||||
}
|
||||
|
||||
$factory = new \App\Libraries\Abdm\Fhir\FhirGeneratorFactory();
|
||||
$res = $factory->wellness()->generate($source);
|
||||
|
||||
$hfrId = (string) ($source['hfr_id'] ?? 'HFR-IN-HMS');
|
||||
$adapter = new \App\Libraries\Abdm\Fhir\Support\GatewayPayloadAdapter();
|
||||
$gatewayPayload = $adapter->toGatewayPayload($res, $source, $hfrId);
|
||||
|
||||
return $this->response->setJSON([
|
||||
'ok' => 1,
|
||||
'source' => $source,
|
||||
'bundle' => $gatewayPayload['fhir_bundle'] ?? [],
|
||||
'fhir_bundle' => $gatewayPayload['fhir_bundle'] ?? [],
|
||||
'validation' => $gatewayPayload['fhir_validation'] ?? [],
|
||||
'care_context_reference' => $gatewayPayload['care_context_reference'] ?? '',
|
||||
'care_context_display' => $gatewayPayload['care_context_display'] ?? '',
|
||||
]);
|
||||
}
|
||||
|
||||
public function buildWellnessRecordSource(int $patientId, int $opdSessionId = 0): array
|
||||
{
|
||||
if ($patientId <= 0 || ! $this->db->tableExists('patient_master')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$patientRow = $this->db->table('patient_master')->where('id', $patientId)->get(1)->getRowArray();
|
||||
if (empty($patientRow)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$rxRow = [];
|
||||
if ($this->db->tableExists('opd_prescription')) {
|
||||
$builder = $this->db->table('opd_prescription')->where('p_id', $patientId);
|
||||
if ($opdSessionId > 0) {
|
||||
$builder->groupStart()
|
||||
->where('id', $opdSessionId)
|
||||
->orWhere('opd_id', $opdSessionId)
|
||||
->groupEnd();
|
||||
}
|
||||
$rxRow = $builder->orderBy('id', 'DESC')->get(1)->getRowArray() ?? [];
|
||||
if (empty($rxRow) && $opdSessionId > 0) {
|
||||
$rxRow = $this->db->table('opd_prescription')->where('p_id', $patientId)->orderBy('id', 'DESC')->get(1)->getRowArray() ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
$doctorRow = [];
|
||||
$drId = (int) ($rxRow['doc_id'] ?? $rxRow['dr_id'] ?? 0);
|
||||
if ($drId > 0 && $this->db->tableExists('doctor_master')) {
|
||||
$doctorRow = $this->db->table('doctor_master')->where('id', $drId)->get(1)->getRowArray() ?? [];
|
||||
}
|
||||
|
||||
$vitals = [];
|
||||
$addVital = static function (string $code, string $display, mixed $val, string $unit, string $ucum) use (&$vitals): void {
|
||||
if ($val !== null && trim((string) $val) !== '') {
|
||||
$vitals[] = [
|
||||
'loinc_code' => $code,
|
||||
'code' => $code,
|
||||
'display' => $display,
|
||||
'value' => (float) $val,
|
||||
'unit' => $unit,
|
||||
'ucum_code' => $ucum,
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
$addVital('8480-6', 'Systolic blood pressure', $rxRow['bp'] ?? null, 'mmHg', 'mm[Hg]');
|
||||
$addVital('8462-4', 'Diastolic blood pressure', $rxRow['diastolic'] ?? null, 'mmHg', 'mm[Hg]');
|
||||
$addVital('8867-4', 'Heart rate', $rxRow['pulse'] ?? null, '/min', '/min');
|
||||
$addVital('8302-2', 'Body height', $rxRow['height'] ?? null, 'cm', 'cm');
|
||||
$addVital('29463-7', 'Body weight', $rxRow['weight'] ?? null, 'kg', 'kg');
|
||||
$addVital('39156-5', 'Body Mass Index', $rxRow['bmi'] ?? null, 'kg/m2', 'kg/m2');
|
||||
|
||||
$tempVal = $rxRow['temp'] ?? null;
|
||||
if ($tempVal !== null && is_numeric($tempVal) && (float) $tempVal > 45) {
|
||||
$tempVal = (((float) $tempVal - 32) * 5) / 9;
|
||||
}
|
||||
$addVital('8310-5', 'Body temperature', $tempVal, 'Cel', 'Cel');
|
||||
$addVital('9279-1', 'Respiratory rate', $rxRow['rr_min'] ?? null, '/min', '/min');
|
||||
$addVital('59408-5', 'Oxygen saturation in Arterial blood by Pulse oximetry', $rxRow['spo2'] ?? null, '%', '%');
|
||||
$addVital('2339-0', 'Blood Glucose', $rxRow['glucose'] ?? null, 'mg/dL', 'mg/dL');
|
||||
|
||||
$physicalExam = [];
|
||||
foreach (['complaints', 'diagnosis', 'investigation'] as $f) {
|
||||
$val = trim((string) ($rxRow[$f] ?? ''));
|
||||
if ($val !== '') {
|
||||
$physicalExam[] = ucwords($f) . ': ' . $val;
|
||||
}
|
||||
}
|
||||
|
||||
$womenWellness = [];
|
||||
$lmp = trim((string) ($rxRow['lmp'] ?? $patientRow['lmp'] ?? ''));
|
||||
if ($lmp !== '') {
|
||||
$womenWellness['lmp'] = $lmp;
|
||||
}
|
||||
foreach (['gravida', 'para', 'abortion', 'living_children', 'menstrual_history'] as $f) {
|
||||
$val = trim((string) ($rxRow[$f] ?? $patientRow[$f] ?? ''));
|
||||
if ($val !== '') {
|
||||
$womenWellness[$f] = $val;
|
||||
}
|
||||
}
|
||||
|
||||
$lifestyle = [];
|
||||
foreach (['is_smoking' => 'Smoking status', 'is_alcohol' => 'Alcohol use', 'is_tobacoo' => 'Tobacco use'] as $field => $label) {
|
||||
$val = trim((string) ($patientRow[$field] ?? ''));
|
||||
if ($val !== '' && $val !== '0') {
|
||||
$lifestyle[] = $label . ': Yes';
|
||||
}
|
||||
}
|
||||
$adviceStr = trim((string) ($rxRow['advice'] ?? ''));
|
||||
if ($adviceStr !== '') {
|
||||
$lifestyle[] = 'Diet & Lifestyle Advice: ' . $adviceStr;
|
||||
}
|
||||
|
||||
$patientName = trim(trim((string) ($patientRow['p_fname'] ?? '')) . ' ' . trim((string) ($patientRow['p_lname'] ?? '')));
|
||||
$abhaIdRaw = '';
|
||||
foreach (['abha_id', 'abha_no', 'abha'] as $field) {
|
||||
$candidate = trim((string) ($patientRow[$field] ?? ''));
|
||||
if ($candidate !== '') {
|
||||
$abhaIdRaw = $candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$abhaDigits = preg_replace('/\D/', '', $abhaIdRaw);
|
||||
$abhaDigits = is_string($abhaDigits) ? $abhaDigits : '';
|
||||
$abhaAddress = trim((string) ($patientRow['abha_address'] ?? ''));
|
||||
|
||||
$hfrId = trim($this->getHospitalSettingValue('ABDM_HFR_ID'));
|
||||
if ($hfrId === '') {
|
||||
$hfrId = 'HFR-IN-HMS';
|
||||
}
|
||||
$hospitalName = $this->getHospitalSettingValue('H_Name');
|
||||
if ($hospitalName === '' && defined('H_Name')) {
|
||||
$hospitalName = (string) constant('H_Name');
|
||||
}
|
||||
|
||||
$doctorName = trim(trim((string) ($doctorRow['p_fname'] ?? '')) . ' ' . trim((string) ($doctorRow['p_lname'] ?? '')));
|
||||
if ($doctorName === '') {
|
||||
$doctorName = 'Doctor';
|
||||
}
|
||||
|
||||
$visitDate = ! empty($rxRow['date_opd_visit']) ? date('Y-m-d', strtotime((string) $rxRow['date_opd_visit'])) : date('Y-m-d');
|
||||
$completedAt = ! empty($rxRow['insert_date']) ? date(DATE_ATOM, strtotime((string) $rxRow['insert_date'])) : date(DATE_ATOM);
|
||||
|
||||
return [
|
||||
'record_id' => (string) ($rxRow['id'] ?? $patientId),
|
||||
'session_id' => (string) $opdSessionId,
|
||||
'visit_date' => $visitDate,
|
||||
'completed_at' => $completedAt,
|
||||
'vitals' => $vitals,
|
||||
'physical_examination' => $physicalExam,
|
||||
'women_wellness' => $womenWellness,
|
||||
'advice' => $lifestyle,
|
||||
'doctor_name' => $doctorName,
|
||||
'hfr_id' => $hfrId,
|
||||
'organization' => [
|
||||
'id' => $hfrId,
|
||||
'name' => $hospitalName,
|
||||
],
|
||||
'patient' => [
|
||||
'id' => (string) $patientId,
|
||||
'uhid' => (string) ($patientRow['uhid_no'] ?? $patientRow['uhid'] ?? $patientRow['patient_code'] ?? $patientId),
|
||||
'name' => $patientName,
|
||||
'gender' => strtolower((string) ($patientRow['gender'] ?? '')) === 'm' ? 'male' : (strtolower((string) ($patientRow['gender'] ?? '')) === 'f' ? 'female' : 'unknown'),
|
||||
'dob' => ! empty($patientRow['dob']) ? date('Y-m-d', strtotime((string) $patientRow['dob'])) : '',
|
||||
'abha_id' => $abhaDigits,
|
||||
'abha_address' => $abhaAddress,
|
||||
'mobile' => (string) ($patientRow['mphone1'] ?? ''),
|
||||
],
|
||||
'practitioner' => [
|
||||
'id' => (string) $drId,
|
||||
'name' => $doctorName,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function patient_search()
|
||||
|
|
@ -1286,15 +1823,7 @@ class DoctorDocument extends BaseController
|
|||
$patientDoc
|
||||
);
|
||||
|
||||
$logoFile = defined('H_logo') ? (string) constant('H_logo') : 'logo.png';
|
||||
$logoAbsPath = FCPATH . 'assets/images/' . $logoFile;
|
||||
$hospitalLogoSrc = $this->buildImageDataUriFromPath($logoAbsPath);
|
||||
if ($hospitalLogoSrc === '' && is_file($logoAbsPath)) {
|
||||
$hospitalLogoSrc = str_replace('\\', '/', $logoAbsPath);
|
||||
}
|
||||
if ($hospitalLogoSrc === '') {
|
||||
$hospitalLogoSrc = base_url('assets/images/' . $logoFile);
|
||||
}
|
||||
$hospitalLogoSrc = $this->resolveHospitalLogoDataUri();
|
||||
|
||||
$data = [
|
||||
'content' => $content,
|
||||
|
|
@ -1483,14 +2012,10 @@ class DoctorDocument extends BaseController
|
|||
|
||||
private function getHospitalSettingValue(string $key): string
|
||||
{
|
||||
if ($this->db->tableExists('hospital_setting')) {
|
||||
$row = $this->db->table('hospital_setting')
|
||||
->select('value')
|
||||
->where('title', $key)
|
||||
->get(1)
|
||||
->getRowArray();
|
||||
if (isset($row['value']) && trim((string) $row['value']) !== '') {
|
||||
return trim((string) $row['value']);
|
||||
if (function_exists('hospital_setting_value')) {
|
||||
$val = hospital_setting_value($key, '');
|
||||
if ($val !== '') {
|
||||
return $val;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1508,7 +2033,13 @@ class DoctorDocument extends BaseController
|
|||
}
|
||||
|
||||
$patientDoc = $this->db->table('patient_doc')->where('id', $patientDocId)->get(1)->getRowArray();
|
||||
if (! is_array($patientDoc)) {
|
||||
if (! is_array($patientDoc) || empty($patientDoc)) {
|
||||
if ($this->db->tableExists('file_upload_data')) {
|
||||
$fileRow = $this->db->table('file_upload_data')->where('id', $patientDocId)->get(1)->getRowArray();
|
||||
if (is_array($fileRow) && ! empty($fileRow)) {
|
||||
return $this->buildFileUploadHealthDocumentSource($fileRow);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
|
|
@ -1563,12 +2094,29 @@ class DoctorDocument extends BaseController
|
|||
$doctorName = 'Doctor';
|
||||
}
|
||||
|
||||
$pdfBytes = $this->generatePatientDocPdfBytes($patientDocId);
|
||||
$docDataBase64 = ($pdfBytes !== null && $pdfBytes !== '') ? base64_encode($pdfBytes) : '';
|
||||
$contentType = ($pdfBytes !== null && $pdfBytes !== '') ? 'application/pdf' : 'text/html';
|
||||
|
||||
if ($docDataBase64 === '') {
|
||||
$rawHtml = trim((string) ($patientDoc['raw_data'] ?? ''));
|
||||
if ($rawHtml !== '') {
|
||||
$docDataBase64 = base64_encode('<div xmlns="http://www.w3.org/1999/xhtml">' . $rawHtml . '</div>');
|
||||
$contentType = 'text/html';
|
||||
} else {
|
||||
$docDataBase64 = base64_encode('<div xmlns="http://www.w3.org/1999/xhtml"><h3>' . esc($docTitle) . '</h3><p>Patient Document #' . $patientDocId . '</p></div>');
|
||||
$contentType = 'text/html';
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'record_id' => (string) $patientDocId,
|
||||
'session_id' => (string) $patientDocId,
|
||||
'visit_date' => $visitDate,
|
||||
'completed_at' => $issueIso,
|
||||
'document_title' => $docTitle,
|
||||
'document_data_base64' => $docDataBase64,
|
||||
'content_type' => $contentType,
|
||||
'document_content_html' => (string) ($patientDoc['raw_data'] ?? ''),
|
||||
'doctor_name' => $doctorName,
|
||||
'hfr_id' => $hfrId,
|
||||
|
|
@ -1593,6 +2141,261 @@ class DoctorDocument extends BaseController
|
|||
];
|
||||
}
|
||||
|
||||
private function generatePatientDocPdfBytes(int $patientDocId): ?string
|
||||
{
|
||||
if ($patientDocId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->ensurePrintTemplateColumns();
|
||||
$this->ensureDocumentPrintTemplateTable();
|
||||
|
||||
$patientDoc = $this->db->table('patient_doc pd')
|
||||
->select('pd.*,dm.doc_name,dm.doc_desc,dm.default_print_type,dm.print_top_margin,dm.print_bottom_margin,dm.print_left_margin,dm.print_right_margin,dm.print_header_margin,dm.print_footer_margin,p.p_fname,p.p_relative,p.p_rname,p.p_code,p.gender,p.age,p.age_in_month,p.estimate_dob,p.dob,dr.p_fname as dr_name')
|
||||
->join('doc_format_master dm', 'pd.doc_format_id=dm.df_id', 'left')
|
||||
->join('patient_master p', 'pd.p_id=p.id', 'left')
|
||||
->join('doctor_master dr', 'pd.dr_id=dr.id', 'left')
|
||||
->where('pd.id', $patientDocId)
|
||||
->get(1)
|
||||
->getRowArray();
|
||||
|
||||
if (! is_array($patientDoc)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$selectedPrintTemplate = null;
|
||||
if ($this->db->tableExists('doc_print_templates')) {
|
||||
$selectedPrintTemplate = $this->db->table('doc_print_templates')
|
||||
->where('status', 1)
|
||||
->where('is_default', 1)
|
||||
->orderBy('id', 'DESC')
|
||||
->get(1)
|
||||
->getRowArray();
|
||||
}
|
||||
|
||||
$resolvedPrintType = is_array($selectedPrintTemplate)
|
||||
? (int) ($selectedPrintTemplate['print_on_type'] ?? 1)
|
||||
: (int) ($patientDoc['default_print_type'] ?? 0);
|
||||
$resolvedPrintType = ((int) $resolvedPrintType === 1) ? 1 : 0;
|
||||
|
||||
if (is_array($selectedPrintTemplate)) {
|
||||
$printTopMargin = $this->normalizeMarginValue($selectedPrintTemplate['page_margin_top_cm'] ?? null, 6.10);
|
||||
$printBottomMargin = $this->normalizeMarginValue($selectedPrintTemplate['page_margin_bottom_cm'] ?? null, 2.50);
|
||||
$printLeftMargin = $this->normalizeMarginValue($selectedPrintTemplate['page_margin_left_cm'] ?? null, 0.70);
|
||||
$printRightMargin = $this->normalizeMarginValue($selectedPrintTemplate['page_margin_right_cm'] ?? null, 0.70);
|
||||
$printHeaderMargin = $this->normalizeMarginValue($selectedPrintTemplate['margin_header_cm'] ?? null, 0.50);
|
||||
$printFooterMargin = $this->normalizeMarginValue($selectedPrintTemplate['margin_footer_cm'] ?? null, 1.50);
|
||||
$pageSize = trim((string) ($selectedPrintTemplate['page_size'] ?? 'A4')) ?: 'A4';
|
||||
} else {
|
||||
$printTopMargin = $this->normalizeMarginValue($patientDoc['print_top_margin'] ?? null, 6.10);
|
||||
$printBottomMargin = $this->normalizeMarginValue($patientDoc['print_bottom_margin'] ?? null, 2.50);
|
||||
$printLeftMargin = $this->normalizeMarginValue($patientDoc['print_left_margin'] ?? null, 0.70);
|
||||
$printRightMargin = $this->normalizeMarginValue($patientDoc['print_right_margin'] ?? null, 0.70);
|
||||
$printHeaderMargin = $this->normalizeMarginValue($patientDoc['print_header_margin'] ?? null, 0.50);
|
||||
$printFooterMargin = $this->normalizeMarginValue($patientDoc['print_footer_margin'] ?? null, 1.50);
|
||||
$pageSize = 'A4';
|
||||
}
|
||||
|
||||
$issueDate = ! empty($patientDoc['date_issue']) ? date('d-m-Y', strtotime((string) $patientDoc['date_issue'])) : date('d-m-Y');
|
||||
$printNo = 1;
|
||||
if ($this->db->tableExists('file_upload_data')) {
|
||||
$printNo = (int) $this->db->table('file_upload_data')->where('doc_id', $patientDocId)->countAllResults() + 1;
|
||||
}
|
||||
|
||||
$headerRef = 'Document Ref. No.' . date('Y') . '/' . $printNo . '/' . $patientDocId;
|
||||
$content = '<table border="0" cellpadding="1" cellspacing="1" style="width:100%"><tbody><tr><td>'
|
||||
. $headerRef . '</td><td style="text-align:right">Date : ' . $issueDate . '</td></tr></tbody></table>';
|
||||
|
||||
$rawData = (string) ($patientDoc['raw_data'] ?? '');
|
||||
$rawData = str_replace(["\\r\\n", "\\n", "\\r"], "\n", $rawData);
|
||||
$rawData = $this->resolveDocPrintTemplatePlaceholders($rawData, $patientDoc);
|
||||
$content .= $rawData;
|
||||
|
||||
$customHeaderHtml = $this->resolveDocPrintTemplatePlaceholders((string) ($selectedPrintTemplate['header_html'] ?? ''), $patientDoc);
|
||||
$customFooterHtml = $this->resolveDocPrintTemplatePlaceholders((string) ($selectedPrintTemplate['footer_html'] ?? ''), $patientDoc);
|
||||
|
||||
$hospitalLogoSrc = $this->resolveHospitalLogoDataUri();
|
||||
|
||||
$data = [
|
||||
'content' => $content,
|
||||
'print_on_type' => $resolvedPrintType,
|
||||
'bar_content' => $headerRef . '/' . $issueDate,
|
||||
'doctor_name' => (string) ($patientDoc['dr_name'] ?? ''),
|
||||
'report_title' => (string) ($patientDoc['doc_name'] ?? 'Patient Document'),
|
||||
'print_top_margin' => $printTopMargin,
|
||||
'print_bottom_margin' => $printBottomMargin,
|
||||
'print_left_margin' => $printLeftMargin,
|
||||
'print_right_margin' => $printRightMargin,
|
||||
'print_header_margin' => $printHeaderMargin,
|
||||
'print_footer_margin' => $printFooterMargin,
|
||||
'custom_header_html' => $customHeaderHtml,
|
||||
'custom_footer_html' => $customFooterHtml,
|
||||
'has_selected_print_template' => is_array($selectedPrintTemplate),
|
||||
'hospital_logo_src' => $hospitalLogoSrc,
|
||||
];
|
||||
|
||||
$mpdfTempDir = WRITEPATH . 'cache' . DIRECTORY_SEPARATOR . 'mpdf';
|
||||
if (! is_dir($mpdfTempDir)) {
|
||||
mkdir($mpdfTempDir, 0755, true);
|
||||
}
|
||||
|
||||
$mpdfFontDir = realpath(__DIR__ . '/../../vendor/mpdf/mpdf/ttfonts');
|
||||
$mpdfFontDirs = $mpdfFontDir !== false ? [$mpdfFontDir] : [];
|
||||
|
||||
$mpdf = new Mpdf([
|
||||
'format' => $pageSize,
|
||||
'margin_top' => $printTopMargin * 10,
|
||||
'margin_bottom' => $printBottomMargin * 10,
|
||||
'margin_left' => $printLeftMargin * 10,
|
||||
'margin_right' => $printRightMargin * 10,
|
||||
'margin_header' => $printHeaderMargin * 10,
|
||||
'margin_footer' => $printFooterMargin * 10,
|
||||
'tempDir' => $mpdfTempDir,
|
||||
'default_font' => 'freesans',
|
||||
'fontDir' => $mpdfFontDirs,
|
||||
'fontdata' => [
|
||||
'freesans' => [
|
||||
'R' => 'FreeSans.ttf',
|
||||
'B' => 'FreeSansBold.ttf',
|
||||
'I' => 'FreeSansOblique.ttf',
|
||||
'BI' => 'FreeSansBoldOblique.ttf',
|
||||
'useOTL' => 0xFF,
|
||||
'useKashida' => 75,
|
||||
],
|
||||
],
|
||||
'autoScriptToLang' => true,
|
||||
'autoLanguageDetection' => true,
|
||||
]);
|
||||
|
||||
if ($resolvedPrintType === 1) {
|
||||
$hospitalName = defined('H_Name') ? (string) constant('H_Name') : 'Hospital';
|
||||
$mpdf->SetWatermarkText($hospitalName, 0.1);
|
||||
$mpdf->showWatermarkText = true;
|
||||
}
|
||||
|
||||
$html = view('doctor_document/doc_letterhead_print', $data);
|
||||
$html = mpdf_normalize_font_weight_css($html);
|
||||
$mpdf->WriteHTML($html, HTMLParserMode::DEFAULT_MODE);
|
||||
|
||||
return $mpdf->Output('', 'S');
|
||||
} catch (\Throwable $e) {
|
||||
log_message('warning', 'Unable to generate PDF for patient_doc {id}: {msg}', ['id' => $patientDocId, 'msg' => $e->getMessage()]);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private function buildFileUploadHealthDocumentSource(array $fileRow): array
|
||||
{
|
||||
$fileUploadId = (int) ($fileRow['id'] ?? 0);
|
||||
$pid = (int) ($fileRow['pid'] ?? 0);
|
||||
if ($pid <= 0 && ! empty($fileRow['opd_id']) && $this->db->tableExists('opd_master')) {
|
||||
$opdRow = $this->db->table('opd_master')->where('opd_id', (int) $fileRow['opd_id'])->get(1)->getRowArray();
|
||||
$pid = (int) ($opdRow['p_id'] ?? 0);
|
||||
}
|
||||
if ($pid <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$patientRow = $this->db->table('patient_master')->where('id', $pid)->get(1)->getRowArray();
|
||||
if (empty($patientRow)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$filePath = $this->resolveFileUploadPath($fileRow);
|
||||
|
||||
$bytes = '';
|
||||
$mimeType = 'application/pdf';
|
||||
if ($filePath !== '' && is_file($filePath) && is_readable($filePath)) {
|
||||
$bytes = (string) @file_get_contents($filePath);
|
||||
$ext = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
|
||||
$mimeType = match ($ext) {
|
||||
'pdf' => 'application/pdf',
|
||||
'png' => 'image/png',
|
||||
'webp' => 'image/webp',
|
||||
'jpg', 'jpeg' => 'image/jpeg',
|
||||
default => (string) (mime_content_type($filePath) ?: 'application/octet-stream'),
|
||||
};
|
||||
}
|
||||
|
||||
$docTitle = trim((string) ($fileRow['document_type'] ?? $fileRow['scan_type'] ?? $fileRow['content_description'] ?? ''));
|
||||
if ($docTitle === '' || strcasecmp($docTitle, 'Queued for AI analysis') === 0) {
|
||||
$docTitle = 'Patient Scanned Document';
|
||||
}
|
||||
|
||||
if ($bytes === '') {
|
||||
$bytes = '<div xmlns="http://www.w3.org/1999/xhtml"><h3>' . esc($docTitle) . '</h3><p>Uploaded Document ID: ' . $fileUploadId . '</p></div>';
|
||||
$mimeType = 'text/html';
|
||||
}
|
||||
|
||||
$patientName = trim(trim((string) ($patientRow['p_fname'] ?? '')) . ' ' . trim((string) ($patientRow['p_lname'] ?? '')));
|
||||
$abhaIdRaw = '';
|
||||
foreach (['abha_id', 'abha_no', 'abha'] as $field) {
|
||||
$candidate = trim((string) ($patientRow[$field] ?? ''));
|
||||
if ($candidate !== '') {
|
||||
$abhaIdRaw = $candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$abhaDigits = preg_replace('/\D/', '', $abhaIdRaw);
|
||||
$abhaDigits = is_string($abhaDigits) ? $abhaDigits : '';
|
||||
$abhaAddress = trim((string) ($patientRow['abha_address'] ?? ''));
|
||||
|
||||
$hfrId = trim($this->getHospitalSettingValue('ABDM_HFR_ID'));
|
||||
if ($hfrId === '') {
|
||||
$hfrId = 'HFR-IN-HMS';
|
||||
}
|
||||
$hospitalName = $this->getHospitalSettingValue('H_Name');
|
||||
if ($hospitalName === '' && defined('H_Name')) {
|
||||
$hospitalName = (string) constant('H_Name');
|
||||
}
|
||||
|
||||
$visitDate = ! empty($fileRow['insert_date']) ? date('Y-m-d', strtotime((string) $fileRow['insert_date'])) : date('Y-m-d');
|
||||
$completedAt = ! empty($fileRow['insert_date']) ? date(DATE_ATOM, strtotime((string) $fileRow['insert_date'])) : date(DATE_ATOM);
|
||||
|
||||
$drId = (int) ($fileRow['doc_id'] ?? $fileRow['upload_by_id'] ?? 1);
|
||||
$drName = 'Doctor';
|
||||
if ($drId > 0 && $this->db->tableExists('doctor_master')) {
|
||||
$docRow = $this->db->table('doctor_master')->where('id', $drId)->get(1)->getRowArray();
|
||||
if (is_array($docRow)) {
|
||||
$drName = trim(trim((string) ($docRow['p_fname'] ?? '')) . ' ' . trim((string) ($docRow['p_lname'] ?? '')));
|
||||
}
|
||||
}
|
||||
if ($drName === '' || $drName === 'Doctor') {
|
||||
$drName = 'Medical Officer';
|
||||
}
|
||||
|
||||
return [
|
||||
'record_id' => 'file-' . $fileUploadId,
|
||||
'session_id' => (string) ($fileRow['opd_id'] ?? $fileUploadId),
|
||||
'visit_date' => $visitDate,
|
||||
'completed_at' => $completedAt,
|
||||
'document_title' => $docTitle,
|
||||
'document_data_base64' => $bytes !== '' ? base64_encode($bytes) : '',
|
||||
'content_type' => $mimeType,
|
||||
'doctor_name' => $drName,
|
||||
'hfr_id' => $hfrId,
|
||||
'organization' => [
|
||||
'id' => $hfrId,
|
||||
'name' => $hospitalName,
|
||||
],
|
||||
'patient' => [
|
||||
'id' => (string) $pid,
|
||||
'uhid' => (string) ($patientRow['uhid_no'] ?? $patientRow['uhid'] ?? $patientRow['patient_code'] ?? $pid),
|
||||
'name' => $patientName,
|
||||
'gender' => strtolower((string) ($patientRow['gender'] ?? '')) === 'm' ? 'male' : (strtolower((string) ($patientRow['gender'] ?? '')) === 'f' ? 'female' : 'unknown'),
|
||||
'dob' => ! empty($patientRow['dob']) ? date('Y-m-d', strtotime((string) $patientRow['dob'])) : '',
|
||||
'abha_id' => $abhaDigits,
|
||||
'abha_address' => $abhaAddress,
|
||||
'mobile' => (string) ($patientRow['mphone1'] ?? ''),
|
||||
],
|
||||
'practitioner' => [
|
||||
'id' => (string) ($drId > 0 ? $drId : 1),
|
||||
'name' => $drName,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function enqueueHealthDocumentFhirSync(int $patientDocId): void
|
||||
{
|
||||
if ($patientDocId <= 0) {
|
||||
|
|
@ -1638,7 +2441,7 @@ class DoctorDocument extends BaseController
|
|||
$factory = new \App\Libraries\Abdm\Fhir\FhirGeneratorFactory();
|
||||
$generatorOutput = $factory->healthDocument()->generate($source);
|
||||
|
||||
$adapter = new \App\Libraries\Abdm\Sync\GatewayPayloadAdapter();
|
||||
$adapter = new \App\Libraries\Abdm\Fhir\Support\GatewayPayloadAdapter();
|
||||
$gatewayPayload = $adapter->toGatewayPayload($generatorOutput, $source, $hfrId);
|
||||
|
||||
$syncPayload = [
|
||||
|
|
@ -1665,4 +2468,54 @@ class DoctorDocument extends BaseController
|
|||
$outbox = new \App\Libraries\Abdm\Sync\AbdmSyncOutboxService();
|
||||
$outbox->enqueueRecordSync($syncPayload);
|
||||
}
|
||||
|
||||
private function resolveFileUploadPath(array $fileRow): string
|
||||
{
|
||||
$fileName = trim((string) ($fileRow['file_name'] ?? $fileRow['orig_name'] ?? ''));
|
||||
$fullPath = trim((string) ($fileRow['full_path'] ?? ''));
|
||||
$dirPath = trim((string) ($fileRow['file_path'] ?? ''));
|
||||
|
||||
$candidates = [];
|
||||
if ($fullPath !== '') {
|
||||
$candidates[] = $fullPath;
|
||||
$candidates[] = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $fullPath);
|
||||
}
|
||||
if ($dirPath !== '' && $fileName !== '') {
|
||||
$candidates[] = rtrim($dirPath, '/\\') . DIRECTORY_SEPARATOR . $fileName;
|
||||
}
|
||||
|
||||
$baseDirs = [
|
||||
defined('FCPATH') ? FCPATH : '',
|
||||
defined('FCPATH') ? FCPATH . 'uploads' : '',
|
||||
defined('ROOTPATH') ? ROOTPATH . 'public' . DIRECTORY_SEPARATOR . 'uploads' : '',
|
||||
];
|
||||
|
||||
$insertDate = ! empty($fileRow['insert_date']) ? (string) $fileRow['insert_date'] : '';
|
||||
$dateSubdir = $insertDate !== '' ? date('Ymd', strtotime($insertDate)) : '';
|
||||
|
||||
if ($fileName !== '') {
|
||||
foreach ($baseDirs as $base) {
|
||||
if ($base === '') continue;
|
||||
if ($dateSubdir !== '') {
|
||||
$candidates[] = rtrim($base, '/\\') . DIRECTORY_SEPARATOR . $dateSubdir . DIRECTORY_SEPARATOR . $fileName;
|
||||
}
|
||||
$candidates[] = rtrim($base, '/\\') . DIRECTORY_SEPARATOR . $fileName;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($candidates as $path) {
|
||||
if ($path !== '' && is_file($path) && is_readable($path)) {
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
|
||||
if ($fileName !== '' && defined('FCPATH')) {
|
||||
$matches = glob(rtrim(FCPATH, '/\\') . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . '*' . DIRECTORY_SEPARATOR . $fileName);
|
||||
if (! empty($matches) && is_file($matches[0]) && is_readable($matches[0])) {
|
||||
return $matches[0];
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1486,7 +1486,35 @@ class Patient extends BaseController
|
|||
}
|
||||
|
||||
$docRows = $builder->orderBy('id', 'DESC')->get()->getResultArray();
|
||||
$fileIds = array_column($docRows, 'id');
|
||||
|
||||
$syncMap = [];
|
||||
if (! empty($fileIds) && $this->db->tableExists('abdm_sync_records')) {
|
||||
$localIds = array_map(static fn($id) => 'file-upload-' . $id, $fileIds);
|
||||
$syncRows = $this->db->table('abdm_sync_records')
|
||||
->whereIn('local_record_id', $localIds)
|
||||
->get()
|
||||
->getResultArray();
|
||||
foreach ($syncRows as $sRow) {
|
||||
$rawId = str_replace(['file-upload-', 'file-'], '', (string) ($sRow['local_record_id'] ?? ''));
|
||||
$syncMap[(int) $rawId] = $sRow;
|
||||
}
|
||||
}
|
||||
|
||||
$taskMap = [];
|
||||
if (! empty($fileIds) && $this->db->tableExists('abdm_work_tasks')) {
|
||||
$taskRows = $this->db->table('abdm_work_tasks')
|
||||
->where('source_module', 'file_upload_data')
|
||||
->whereIn('source_id', array_map('strval', $fileIds))
|
||||
->get()
|
||||
->getResultArray();
|
||||
foreach ($taskRows as $tRow) {
|
||||
$taskMap[(int) ($tRow['source_id'] ?? 0)] = $tRow;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($docRows as $row) {
|
||||
$docId = (int) ($row['id'] ?? 0);
|
||||
$path = (string) ($row['full_path'] ?? $row['public_path'] ?? '');
|
||||
if ($path !== '') {
|
||||
$pos = strpos(str_replace('\\', '/', $path), '/uploads/');
|
||||
|
|
@ -1497,19 +1525,27 @@ class Patient extends BaseController
|
|||
|
||||
$ext = strtolower(ltrim((string) ($row['file_ext'] ?? pathinfo($path, PATHINFO_EXTENSION)), '.'));
|
||||
$isPdf = $ext === 'pdf';
|
||||
$title = trim((string) ($row['document_type'] ?? $row['scan_type'] ?? $row['content_description'] ?? 'Scanned Document'));
|
||||
if ($title === '' || strcasecmp($title, 'Queued for AI analysis') === 0) {
|
||||
$title = 'Scanned Document';
|
||||
$docType = trim((string) ($row['document_type'] ?? $row['scan_type'] ?? $row['content_description'] ?? 'Paper Document (General)'));
|
||||
if ($docType === '' || strcasecmp($docType, 'Queued for AI analysis') === 0) {
|
||||
$docType = 'Paper Document (General)';
|
||||
}
|
||||
|
||||
$hasSync = isset($syncMap[$docId]);
|
||||
$hasTask = isset($taskMap[$docId]);
|
||||
$fhirStatus = $hasSync ? 'FHIR Shared' : ($hasTask ? 'FHIR Ready' : 'FHIR Available');
|
||||
|
||||
$patientDocuments[] = [
|
||||
'id' => (int) ($row['id'] ?? 0),
|
||||
'title' => $title,
|
||||
'id' => $docId,
|
||||
'title' => $docType,
|
||||
'document_type' => $docType,
|
||||
'path' => $path,
|
||||
'isPdf' => $isPdf,
|
||||
'ext' => $ext,
|
||||
'insertDate' => ! empty($row['insert_date']) ? date('d-m-Y H:i', strtotime((string) $row['insert_date'])) : '',
|
||||
'uploadedBy' => (string) ($row['upload_by'] ?? ''),
|
||||
'hasSync' => $hasSync,
|
||||
'hasTask' => $hasTask,
|
||||
'fhirStatus' => $fhirStatus,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -5350,12 +5386,9 @@ class Patient extends BaseController
|
|||
$abhaDigits = is_string($abhaDigits) ? $abhaDigits : '';
|
||||
$abhaAddress = trim((string) ($patientRow['abha_address'] ?? ''));
|
||||
|
||||
$hfrId = 'HFR-IN-HMS';
|
||||
if ($this->db->tableExists('hospital_setting')) {
|
||||
$hfrRow = $this->db->table('hospital_setting')->where('title', 'ABDM_HFR_ID')->get(1)->getRowArray();
|
||||
if (! empty($hfrRow['value'])) {
|
||||
$hfrId = trim((string) $hfrRow['value']);
|
||||
}
|
||||
$hfrId = trim(hospital_setting_value('ABDM_HFR_ID', 'HFR-IN-HMS'));
|
||||
if ($hfrId === '') {
|
||||
$hfrId = 'HFR-IN-HMS';
|
||||
}
|
||||
$hospitalName = defined('H_Name') ? (string) constant('H_Name') : 'Hospital';
|
||||
|
||||
|
|
@ -5394,7 +5427,7 @@ class Patient extends BaseController
|
|||
$factory = new \App\Libraries\Abdm\Fhir\FhirGeneratorFactory();
|
||||
$generatorOutput = $factory->healthDocument()->generate($source);
|
||||
|
||||
$adapter = new \App\Libraries\Abdm\Sync\GatewayPayloadAdapter();
|
||||
$adapter = new \App\Libraries\Abdm\Fhir\Support\GatewayPayloadAdapter();
|
||||
$gatewayPayload = $adapter->toGatewayPayload($generatorOutput, $source, $hfrId);
|
||||
|
||||
if ($pid > 0 && preg_match('/^\d{14}$/', $abhaDigits) === 1 && class_exists('\App\Libraries\Abdm\Task\AbdmTaskService')) {
|
||||
|
|
@ -5445,5 +5478,149 @@ class Patient extends BaseController
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function generate_fhir_for_file(int $fileId)
|
||||
{
|
||||
if ($fileId <= 0) {
|
||||
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid file ID']);
|
||||
}
|
||||
|
||||
$ok = $this->enqueueHealthDocumentFhirForFileUpload($fileId);
|
||||
if (! $ok) {
|
||||
return $this->response->setStatusCode(500)->setJSON(['ok' => 0, 'error_text' => 'Unable to generate FHIR HealthDocumentRecord for file #' . $fileId]);
|
||||
}
|
||||
|
||||
return $this->response->setJSON([
|
||||
'ok' => 1,
|
||||
'file_id' => $fileId,
|
||||
'message' => 'HealthDocumentRecord FHIR bundle generated & enqueued for ABDM sync',
|
||||
'csrfName' => csrf_token(),
|
||||
'csrfHash' => csrf_hash(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function generate_fhir_bundle_for_all_files(int $pno)
|
||||
{
|
||||
if ($pno <= 0 || ! $this->db->tableExists('file_upload_data')) {
|
||||
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid patient ID or file_upload_data table missing']);
|
||||
}
|
||||
|
||||
$fields = $this->db->getFieldNames('file_upload_data') ?? [];
|
||||
$builder = $this->db->table('file_upload_data');
|
||||
if (in_array('pid', $fields, true)) {
|
||||
$builder->where('pid', $pno);
|
||||
}
|
||||
if (in_array('show_type', $fields, true)) {
|
||||
$builder->where('show_type', 0);
|
||||
}
|
||||
|
||||
$rows = $builder->select('id')->get()->getResultArray();
|
||||
$count = 0;
|
||||
foreach ($rows as $row) {
|
||||
$fileId = (int) ($row['id'] ?? 0);
|
||||
if ($fileId > 0 && $this->enqueueHealthDocumentFhirForFileUpload($fileId)) {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->response->setJSON([
|
||||
'ok' => 1,
|
||||
'count' => $count,
|
||||
'message' => 'Generated FHIR HealthDocumentRecord bundles for ' . $count . ' scanned document(s)',
|
||||
'csrfName' => csrf_token(),
|
||||
'csrfHash' => csrf_hash(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function enqueueWellnessRecordFhir(int $patientId, int $opdSessionId = 0): bool
|
||||
{
|
||||
if ($patientId <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$docCtrl = new \App\Controllers\DoctorDocument();
|
||||
$source = $docCtrl->buildWellnessRecordSource($patientId, $opdSessionId);
|
||||
if (empty($source)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$factory = new \App\Libraries\Abdm\Fhir\FhirGeneratorFactory();
|
||||
$generatorOutput = $factory->wellness()->generate($source);
|
||||
|
||||
$hfrId = (string) ($source['hfr_id'] ?? 'HFR-IN-HMS');
|
||||
$adapter = new \App\Libraries\Abdm\Fhir\Support\GatewayPayloadAdapter();
|
||||
$gatewayPayload = $adapter->toGatewayPayload($generatorOutput, $source, $hfrId);
|
||||
|
||||
$patientName = (string) ($source['patient']['name'] ?? 'Patient');
|
||||
$abhaDigits = (string) ($source['patient']['abha_id'] ?? '');
|
||||
|
||||
if ($patientId > 0 && preg_match('/^\d{14}$/', $abhaDigits) === 1 && class_exists('\App\Libraries\Abdm\Task\AbdmTaskService')) {
|
||||
try {
|
||||
$taskService = new \App\Libraries\Abdm\Task\AbdmTaskService();
|
||||
$taskService->createOrRefreshTask(
|
||||
'wellness_record_publish',
|
||||
'opd_prescription',
|
||||
'opd_vitals',
|
||||
(string) ($source['record_id'] ?? $patientId),
|
||||
$patientId,
|
||||
$patientName,
|
||||
$abhaDigits,
|
||||
'submit',
|
||||
[
|
||||
'opd_session_id' => $opdSessionId,
|
||||
'trigger' => 'vitals.saved',
|
||||
]
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
log_message('warning', 'Unable to refresh ABDM wellness task for patient {id}: {msg}', ['id' => $patientId, 'msg' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
$syncPayload = [
|
||||
'local_record_id' => 'wellness-' . $patientId . ($opdSessionId > 0 ? ('-' . $opdSessionId) : ''),
|
||||
'local_patient_id' => $patientId,
|
||||
'hi_type' => 'WellnessRecord',
|
||||
'care_context_reference' => (string) ($gatewayPayload['care_context_reference'] ?? ('WELLNESS-' . $patientId)),
|
||||
'care_context_display' => (string) ($gatewayPayload['care_context_display'] ?? ('Wellness Record ' . date('d/m/Y'))),
|
||||
'visit_date' => (string) ($source['visit_date'] ?? date('Y-m-d')),
|
||||
'department' => '',
|
||||
'doctor_name' => (string) ($source['doctor_name'] ?? 'Doctor'),
|
||||
'consent_id' => '',
|
||||
'hfr_id' => $hfrId,
|
||||
'source_updated_at' => date('Y-m-d H:i:s'),
|
||||
'patient_name' => $patientName,
|
||||
'mobile' => (string) ($source['patient']['mobile'] ?? ''),
|
||||
'gender' => (string) ($source['patient']['gender'] ?? ''),
|
||||
'dob' => (string) ($source['patient']['dob'] ?? ''),
|
||||
'abha_id' => $abhaDigits,
|
||||
'abha_address' => (string) ($source['patient']['abha_address'] ?? ''),
|
||||
'fhir_bundle' => (array) ($gatewayPayload['fhir_bundle'] ?? []),
|
||||
];
|
||||
|
||||
$outbox = new \App\Libraries\Abdm\Sync\AbdmSyncOutboxService();
|
||||
$outbox->enqueueRecordSync($syncPayload);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function generate_wellness_fhir(int $patientId, int $opdSessionId = 0)
|
||||
{
|
||||
if ($patientId <= 0) {
|
||||
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid patient ID']);
|
||||
}
|
||||
|
||||
$ok = $this->enqueueWellnessRecordFhir($patientId, $opdSessionId);
|
||||
if (! $ok) {
|
||||
return $this->response->setStatusCode(500)->setJSON(['ok' => 0, 'error_text' => 'Unable to generate WellnessRecord FHIR bundle for patient #' . $patientId]);
|
||||
}
|
||||
|
||||
return $this->response->setJSON([
|
||||
'ok' => 1,
|
||||
'patient_id' => $patientId,
|
||||
'message' => 'WellnessRecord FHIR bundle generated & enqueued for ABDM sync',
|
||||
'csrfName' => csrf_token(),
|
||||
'csrfHash' => csrf_hash(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -678,6 +678,92 @@ class Report extends BaseController
|
|||
return $this->response->setBody($content);
|
||||
}
|
||||
|
||||
public function old_payment_received_report()
|
||||
{
|
||||
return view('report/old_payment_received_report');
|
||||
}
|
||||
|
||||
public function old_payment_received_report_data(string $dateRange = '', int $output = 0)
|
||||
{
|
||||
if ($dateRange === '') {
|
||||
$dateRange = date('Y-m-d\T00:00:00') . 'S' . date('Y-m-d\T23:59:00');
|
||||
}
|
||||
|
||||
[$minRange, $maxRange] = $this->parseDateRange($dateRange);
|
||||
|
||||
$builder = $this->db->table('payment_history p');
|
||||
$builder->select('p.id as payment_id, p.payment_date, inv.inv_date, inv.invoice_code, inv.net_amount')
|
||||
->select('DATEDIFF(DATE(p.payment_date), DATE(inv.inv_date)) as delay_days', false)
|
||||
->select("CONCAT(IFNULL(pat.p_fname,''), ' ', IFNULL(pat.p_lname,'')) as patient_name", false)
|
||||
->select('pat.p_code as patient_code')
|
||||
->select('p.amount as paid_amount, p.update_by')
|
||||
->select("CASE p.payment_mode WHEN 1 THEN 'Cash' WHEN 2 THEN 'Bank' ELSE 'Other' END as pay_mode", false)
|
||||
->join('invoice_master inv', 'p.payof_type = 2 AND inv.id = p.payof_id', 'inner')
|
||||
->join('patient_master pat', 'inv.attach_id = pat.id', 'left')
|
||||
->where('p.credit_debit', 0)
|
||||
->where('p.payment_date >=', $minRange)
|
||||
->where('p.payment_date <=', $maxRange)
|
||||
->where('DATE(inv.inv_date) < DATE(p.payment_date)', null, false)
|
||||
->orderBy('p.payment_date', 'DESC');
|
||||
|
||||
$rows = $builder->get()->getResultArray();
|
||||
|
||||
$totalAmount = 0.0;
|
||||
foreach ($rows as $r) {
|
||||
$totalAmount += (float) ($r['paid_amount'] ?? 0);
|
||||
}
|
||||
|
||||
$data = [
|
||||
'rows' => $rows,
|
||||
'min_range' => $minRange,
|
||||
'max_range' => $maxRange,
|
||||
'total_amount' => $totalAmount,
|
||||
];
|
||||
|
||||
$content = view('report/old_payment_received_report_table', $data);
|
||||
|
||||
if ($output === 1) {
|
||||
ExportExcel($content, 'Old_Payment_Received_' . date('YmdHis'));
|
||||
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,
|
||||
]);
|
||||
|
||||
$pdfHtml = '<style>'
|
||||
. 'body{font-family:Arial,sans-serif;color:#111;font-size:12px;}'
|
||||
. 'table{width:100%;border-collapse:collapse;}'
|
||||
. 'th,td{border:1px solid #9aa4ad;padding:6px 8px;}'
|
||||
. 'th{text-align:left;background:#f5f6f7;}'
|
||||
. '.text-end{text-align:right;}'
|
||||
. '</style>'
|
||||
. '<h3 style="margin:0 0 10px 0;">Old Payment Received Report</h3>'
|
||||
. $content;
|
||||
|
||||
$mpdf->WriteHTML($pdfHtml);
|
||||
$fileName = 'Old_Payment_Received_' . 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);
|
||||
}
|
||||
|
||||
public function report_total_payment_total_amount_show(
|
||||
string $dateRange,
|
||||
string $employeeIds,
|
||||
|
|
@ -1386,7 +1472,59 @@ class Report extends BaseController
|
|||
return $this->response->setBody($content);
|
||||
}
|
||||
|
||||
// ==================== Insurance Credit Reports ====================
|
||||
public function reconciliation_mismatch_report(string $dateRange = '')
|
||||
{
|
||||
if ($dateRange === '') {
|
||||
$dateRange = date('Y-m-d\T00:00:00') . 'S' . date('Y-m-d\T23:59:00');
|
||||
}
|
||||
|
||||
[$minRange, $maxRange] = $this->parseDateRange($dateRange);
|
||||
|
||||
$sql = "SELECT
|
||||
i.id AS invoice_id,
|
||||
i.invoice_code,
|
||||
i.inv_date AS invoice_date,
|
||||
pm.p_fname AS patient_name,
|
||||
pm.p_code AS patient_code,
|
||||
SUM(ROUND(ii.item_amount - (ii.item_amount * i.discount_amount / NULLIF(i.total_amount, 0)), 2)) AS diagnosis_billed_amount,
|
||||
IFNULL(pay.paid_on_date, 0) AS collected_on_date,
|
||||
IFNULL(pay_other.paid_other_date, 0) AS collected_other_dates,
|
||||
i.payment_part_balance AS unpaid_balance,
|
||||
(SUM(ROUND(ii.item_amount - (ii.item_amount * i.discount_amount / NULLIF(i.total_amount, 0)), 2)) - IFNULL(pay.paid_on_date, 0)) AS date_mismatch_diff
|
||||
FROM invoice_master i
|
||||
JOIN invoice_item ii ON i.id = ii.inv_master_id
|
||||
JOIN hc_item_type ht ON ii.item_type = ht.itype_id
|
||||
LEFT JOIN patient_master pm ON i.attach_id = pm.id
|
||||
LEFT JOIN (
|
||||
SELECT payof_id, SUM(amount) AS paid_on_date
|
||||
FROM payment_history
|
||||
WHERE payof_type = 2 AND credit_debit = 0
|
||||
AND payment_date >= ? AND payment_date <= ?
|
||||
GROUP BY payof_id
|
||||
) pay ON i.id = pay.payof_id
|
||||
LEFT JOIN (
|
||||
SELECT payof_id, SUM(amount) AS paid_other_date
|
||||
FROM payment_history
|
||||
WHERE payof_type = 2 AND credit_debit = 0
|
||||
AND (payment_date < ? OR payment_date > ?)
|
||||
GROUP BY payof_id
|
||||
) pay_other ON i.id = pay_other.payof_id
|
||||
WHERE i.invoice_status = 1
|
||||
AND i.inv_date >= ? AND i.inv_date <= ?
|
||||
AND i.ipd_id = 0 AND IFNULL(i.insurance_case_id, 0) = 0
|
||||
GROUP BY i.id
|
||||
HAVING date_mismatch_diff != 0 OR unpaid_balance > 0 OR collected_other_dates > 0
|
||||
ORDER BY i.id DESC";
|
||||
|
||||
$mismatches = $this->db->query($sql, [$minRange, $maxRange, $minRange, $maxRange, $minRange, $maxRange])->getResultArray();
|
||||
|
||||
return $this->response->setJSON([
|
||||
'status' => 'success',
|
||||
'date_range' => ['min' => $minRange, 'max' => $maxRange],
|
||||
'count' => count($mismatches),
|
||||
'mismatches' => $mismatches,
|
||||
]);
|
||||
}
|
||||
|
||||
public function insurance_credit_main()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -191,18 +191,24 @@ if (! function_exists('hospital_setting_value')) {
|
|||
try {
|
||||
$db = db_connect();
|
||||
if ($db && method_exists($db, 'tableExists') && $db->tableExists('hospital_setting')) {
|
||||
$rows = $db->table('hospital_setting')
|
||||
->select('s_name, s_value')
|
||||
->get()
|
||||
->getResultArray();
|
||||
$fields = $db->getFieldNames('hospital_setting') ?? [];
|
||||
$nameCol = in_array('s_name', $fields, true) ? 's_name' : (in_array('title', $fields, true) ? 'title' : '');
|
||||
$valCol = in_array('s_value', $fields, true) ? 's_value' : (in_array('value', $fields, true) ? 'value' : '');
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$key = trim((string) ($row['s_name'] ?? ''));
|
||||
if ($key === '') {
|
||||
continue;
|
||||
if ($nameCol !== '' && $valCol !== '') {
|
||||
$rows = $db->table('hospital_setting')
|
||||
->select("{$nameCol}, {$valCol}")
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$key = trim((string) ($row[$nameCol] ?? ''));
|
||||
if ($key === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$settings[$key] = trim((string) ($row[$valCol] ?? ''));
|
||||
}
|
||||
|
||||
$settings[$key] = trim((string) ($row['s_value'] ?? ''));
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
|
|
|
|||
|
|
@ -35,6 +35,22 @@ abstract class AbstractModuleFhirGenerator
|
|||
protected function buildBasePatient(array $source): array
|
||||
{
|
||||
$patientId = (string) ($source['patient']['id'] ?? '');
|
||||
$rawGender = strtolower(trim((string) ($source['patient']['gender'] ?? $source['patient']['xgender'] ?? $source['gender'] ?? '')));
|
||||
$gender = match ($rawGender) {
|
||||
'm', 'male', '1' => 'male',
|
||||
'f', 'female', '2' => 'female',
|
||||
'o', 'other', '3' => 'other',
|
||||
default => 'male',
|
||||
};
|
||||
|
||||
$dob = trim((string) ($source['patient']['dob'] ?? $source['patient']['p_dob'] ?? $source['patient']['birth_date'] ?? ''));
|
||||
if ($dob === '' && ! empty($source['patient']['age'])) {
|
||||
$age = (int) $source['patient']['age'];
|
||||
if ($age > 0) {
|
||||
$dob = (date('Y') - $age) . '-01-01';
|
||||
}
|
||||
}
|
||||
|
||||
$patient = [
|
||||
'resourceType' => 'Patient',
|
||||
'id' => 'patient-' . $patientId,
|
||||
|
|
@ -48,12 +64,15 @@ abstract class AbstractModuleFhirGenerator
|
|||
],
|
||||
],
|
||||
'name' => [[
|
||||
'text' => (string) ($source['patient']['name'] ?? ''),
|
||||
'text' => (string) ($source['patient']['name'] ?? 'Patient'),
|
||||
]],
|
||||
'gender' => (string) ($source['patient']['gender'] ?? ''),
|
||||
'birthDate' => (string) ($source['patient']['dob'] ?? ''),
|
||||
'gender' => $gender,
|
||||
];
|
||||
|
||||
if ($dob !== '') {
|
||||
$patient['birthDate'] = $dob;
|
||||
}
|
||||
|
||||
$abhaId = preg_replace('/\D/', '', (string) ($source['patient']['abha_id'] ?? ''));
|
||||
if (is_string($abhaId) && $abhaId !== '') {
|
||||
$patient['identifier'][] = [
|
||||
|
|
@ -78,7 +97,7 @@ abstract class AbstractModuleFhirGenerator
|
|||
$abhaAddress = trim((string) ($source['patient']['abha_address'] ?? ''));
|
||||
if ($abhaAddress !== '') {
|
||||
$patient['identifier'][] = [
|
||||
'system' => 'https://abdm.gov.in/health-address',
|
||||
'system' => 'https://healthid.ndhm.gov.in/abha-address',
|
||||
'value' => $abhaAddress,
|
||||
];
|
||||
}
|
||||
|
|
@ -93,22 +112,61 @@ abstract class AbstractModuleFhirGenerator
|
|||
{
|
||||
$recordId = (string) ($source['record_id'] ?? '0');
|
||||
$patientId = (string) ($source['patient']['id'] ?? '0');
|
||||
$patientName = trim((string) ($source['patient']['name'] ?? $source['patient_name'] ?? 'Patient'));
|
||||
if ($patientName === '' || strcasecmp($patientName, 'NA') === 0) {
|
||||
$patientName = 'Patient';
|
||||
}
|
||||
$encounterRef = isset($source['encounter']) ? 'urn:uuid:encounter-' . ((string) ($source['encounter']['id'] ?? $recordId)) : null;
|
||||
|
||||
$drId = (string) ($source['practitioner']['id'] ?? $source['doctor']['id'] ?? '1');
|
||||
if ($drId === '' || $drId === '0') {
|
||||
$drId = '1';
|
||||
}
|
||||
$drName = trim((string) ($source['practitioner']['name'] ?? $source['doctor']['name'] ?? $source['doctor_name'] ?? 'Dr. Attending Medical Officer'));
|
||||
if ($drName === '' || strcasecmp($drName, 'Doctor') === 0 || strcasecmp($drName, 'NA') === 0) {
|
||||
$drName = 'Dr. Attending Medical Officer';
|
||||
} elseif (! str_starts_with(strtolower($drName), 'dr.') && ! str_starts_with(strtolower($drName), 'dr ')) {
|
||||
$drName = 'Dr. ' . $drName;
|
||||
}
|
||||
|
||||
$orgId = (string) ($source['organization']['id'] ?? $source['hfr_id'] ?? 'IN0510000871');
|
||||
if ($orgId === '') {
|
||||
$orgId = 'IN0510000871';
|
||||
}
|
||||
|
||||
$authorRef = 'urn:uuid:practitioner-' . $drId;
|
||||
$custodianRef = 'urn:uuid:organization-' . $orgId;
|
||||
|
||||
$composition = [
|
||||
'resourceType' => 'Composition',
|
||||
'id' => 'composition-' . $recordId,
|
||||
'status' => 'final',
|
||||
'type' => [
|
||||
'coding' => [[
|
||||
'system' => 'http://loinc.org',
|
||||
'code' => $loincCode,
|
||||
'display' => $loincDisplay,
|
||||
]],
|
||||
'coding' => [
|
||||
[
|
||||
'system' => 'http://snomed.info/sct',
|
||||
'code' => '419891008',
|
||||
'display' => 'Record artifact',
|
||||
],
|
||||
[
|
||||
'system' => 'http://loinc.org',
|
||||
'code' => $loincCode,
|
||||
'display' => $loincDisplay,
|
||||
],
|
||||
],
|
||||
'text' => 'Health Document Record',
|
||||
],
|
||||
'title' => $title,
|
||||
'date' => (string) ($source['completed_at'] ?? date(DATE_ATOM)),
|
||||
'subject' => ['reference' => 'urn:uuid:patient-' . $patientId],
|
||||
'subject' => [
|
||||
'reference' => 'urn:uuid:patient-' . $patientId,
|
||||
'display' => $patientName,
|
||||
],
|
||||
'author' => [[
|
||||
'reference' => $authorRef,
|
||||
'display' => $drName,
|
||||
]],
|
||||
'custodian' => ['reference' => $custodianRef],
|
||||
];
|
||||
|
||||
if ($encounterRef !== null) {
|
||||
|
|
@ -174,58 +232,70 @@ abstract class AbstractModuleFhirGenerator
|
|||
/** @param array<string,mixed> $source */
|
||||
protected function buildPractitioner(array $source): ?array
|
||||
{
|
||||
if (! isset($source['doctor']) || ! is_array($source['doctor'])) {
|
||||
return null;
|
||||
$doctor = $source['doctor'] ?? $source['practitioner'] ?? [];
|
||||
if (! is_array($doctor)) {
|
||||
$doctor = [];
|
||||
}
|
||||
|
||||
$doctor = $source['doctor'];
|
||||
$id = trim((string) ($doctor['id'] ?? ''));
|
||||
$name = trim((string) ($doctor['name'] ?? ''));
|
||||
if ($id === '' && $name === '') {
|
||||
return null;
|
||||
$id = trim((string) ($doctor['id'] ?? '1'));
|
||||
if ($id === '' || $id === '0') {
|
||||
$id = '1';
|
||||
}
|
||||
|
||||
$name = trim((string) ($doctor['name'] ?? $source['doctor_name'] ?? ''));
|
||||
if ($name === '' || strcasecmp($name, 'Doctor') === 0 || strcasecmp($name, 'NA') === 0) {
|
||||
$name = 'Dr. Attending Medical Officer';
|
||||
} elseif (! str_starts_with(strtolower($name), 'dr.') && ! str_starts_with(strtolower($name), 'dr ')) {
|
||||
$name = 'Dr. ' . $name;
|
||||
}
|
||||
|
||||
$hprId = trim((string) ($doctor['hpr_id'] ?? $doctor['identifier'] ?? $source['hpr_id'] ?? ''));
|
||||
if ($hprId === '') {
|
||||
$hprId = 'HPR-' . $id;
|
||||
}
|
||||
|
||||
return [
|
||||
'resourceType' => 'Practitioner',
|
||||
'id' => 'practitioner-' . ($id !== '' ? $id : md5($name)),
|
||||
'id' => 'practitioner-' . $id,
|
||||
'meta' => ['profile' => [
|
||||
'https://nrces.in/ndhm/fhir/r4/StructureDefinition/Practitioner',
|
||||
]],
|
||||
'name' => [[
|
||||
'text' => $name,
|
||||
'identifier' => [[
|
||||
'system' => 'https://doctor.ndhm.gov.in',
|
||||
'value' => $hprId,
|
||||
]],
|
||||
'identifier' => $id !== '' ? [[
|
||||
'system' => 'https://hms.local/doctor-id',
|
||||
'value' => $id,
|
||||
]] : null,
|
||||
'name' => [['text' => $name]],
|
||||
];
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $source */
|
||||
protected function buildOrganization(array $source): ?array
|
||||
{
|
||||
if (! isset($source['organization']) || ! is_array($source['organization'])) {
|
||||
return null;
|
||||
$org = $source['organization'] ?? [];
|
||||
if (! is_array($org)) {
|
||||
$org = [];
|
||||
}
|
||||
|
||||
$org = $source['organization'];
|
||||
$id = trim((string) ($org['id'] ?? ''));
|
||||
$name = trim((string) ($org['name'] ?? ''));
|
||||
if ($id === '' && $name === '') {
|
||||
return null;
|
||||
$id = trim((string) ($org['id'] ?? $source['hfr_id'] ?? 'IN0510000871'));
|
||||
if ($id === '') {
|
||||
$id = 'IN0510000871';
|
||||
}
|
||||
$name = trim((string) ($org['name'] ?? 'E-Atria Hospital'));
|
||||
if ($name === '') {
|
||||
$name = 'E-Atria Hospital';
|
||||
}
|
||||
|
||||
return [
|
||||
'resourceType' => 'Organization',
|
||||
'id' => 'organization-' . ($id !== '' ? $id : md5($name)),
|
||||
'id' => 'organization-' . $id,
|
||||
'meta' => ['profile' => [
|
||||
'https://nrces.in/ndhm/fhir/r4/StructureDefinition/Organization',
|
||||
]],
|
||||
'name' => $name,
|
||||
'identifier' => $id !== '' ? [[
|
||||
'system' => 'https://hms.local/organization-id',
|
||||
'identifier' => [[
|
||||
'system' => 'https://facility.ndhm.gov.in',
|
||||
'value' => $id,
|
||||
]] : null,
|
||||
]],
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,24 +20,32 @@ class HealthDocumentFhirGenerator extends AbstractModuleFhirGenerator
|
|||
$docTitle = 'Medical Certificate';
|
||||
}
|
||||
|
||||
$careContextReference = 'DOC-' . $recordId . '-' . $visitDate;
|
||||
$careContextReference = 'DOC-' . $recordId . '-' . $visitDate . '-' . date('His');
|
||||
$careContextDisplay = $docTitle . ' (' . $visitDate . ')';
|
||||
|
||||
$builder = new \App\Libraries\Abdm\Fhir\FhirDocumentBuilder();
|
||||
|
||||
// Build Composition
|
||||
$composition = $this->buildBaseComposition($source, $docTitle, '41988-7', 'Medical statement');
|
||||
$composition['meta'] = ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/HealthDocumentRecord']];
|
||||
|
||||
// Add Section linking to DocumentReference
|
||||
$docRefId = 'health-document-' . $recordId;
|
||||
$composition['section'] = [[
|
||||
'title' => $docTitle,
|
||||
'code' => [
|
||||
'coding' => [[
|
||||
'system' => 'http://loinc.org',
|
||||
'code' => '41988-7',
|
||||
'display' => 'Medical statement',
|
||||
]],
|
||||
'coding' => [
|
||||
[
|
||||
'system' => 'http://snomed.info/sct',
|
||||
'code' => '419891008',
|
||||
'display' => 'Record artifact',
|
||||
],
|
||||
[
|
||||
'system' => 'http://loinc.org',
|
||||
'code' => '41988-7',
|
||||
'display' => 'Medical statement',
|
||||
],
|
||||
],
|
||||
],
|
||||
'entry' => [['reference' => 'urn:uuid:' . $docRefId]],
|
||||
]];
|
||||
|
|
@ -66,7 +74,7 @@ class HealthDocumentFhirGenerator extends AbstractModuleFhirGenerator
|
|||
|
||||
// Attachment payload (PDF base64 or raw HTML/Text content)
|
||||
$docData = (string) ($source['document_data_base64'] ?? '');
|
||||
$contentType = 'application/pdf';
|
||||
$contentType = (string) ($source['content_type'] ?? 'application/pdf');
|
||||
|
||||
if ($docData === '') {
|
||||
$rawContent = (string) ($source['document_content_html'] ?? $source['raw_data'] ?? '');
|
||||
|
|
@ -79,33 +87,81 @@ class HealthDocumentFhirGenerator extends AbstractModuleFhirGenerator
|
|||
}
|
||||
}
|
||||
|
||||
$builder->addDocumentReference([
|
||||
$rawBytes = base64_decode($docData);
|
||||
if ($rawBytes === '' || ! str_starts_with($rawBytes, '%PDF')) {
|
||||
$pdfBytes = $this->convertToPdfBytes($rawBytes, $contentType, $docTitle);
|
||||
if ($pdfBytes !== '') {
|
||||
$rawBytes = $pdfBytes;
|
||||
$docData = base64_encode($pdfBytes);
|
||||
$contentType = 'application/pdf';
|
||||
}
|
||||
} else {
|
||||
$contentType = 'application/pdf';
|
||||
}
|
||||
|
||||
$fileSize = is_string($rawBytes) ? strlen($rawBytes) : 0;
|
||||
$sha1Hash = is_string($rawBytes) && $rawBytes !== '' ? base64_encode(sha1($rawBytes, true)) : '';
|
||||
|
||||
$orgId = (string) ($source['organization']['id'] ?? $source['hfr_id'] ?? 'IN0510000871');
|
||||
$drId = (string) ($source['practitioner']['id'] ?? $source['doctor']['id'] ?? '1');
|
||||
if ($drId === '' || $drId === '0') {
|
||||
$drId = '1';
|
||||
}
|
||||
$drName = trim((string) ($source['practitioner']['name'] ?? $source['doctor']['name'] ?? $source['doctor_name'] ?? 'Dr. Attending Medical Officer'));
|
||||
if ($drName === '' || strcasecmp($drName, 'Doctor') === 0 || strcasecmp($drName, 'NA') === 0) {
|
||||
$drName = 'Dr. Attending Medical Officer';
|
||||
} elseif (! str_starts_with(strtolower($drName), 'dr.') && ! str_starts_with(strtolower($drName), 'dr ')) {
|
||||
$drName = 'Dr. ' . $drName;
|
||||
}
|
||||
|
||||
$authorRef = 'urn:uuid:practitioner-' . $drId;
|
||||
|
||||
$docRef = [
|
||||
'resourceType' => 'DocumentReference',
|
||||
'id' => $docRefId,
|
||||
'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/DocumentReference']],
|
||||
'status' => 'current',
|
||||
'docStatus' => 'final',
|
||||
'type' => [
|
||||
'coding' => [[
|
||||
'system' => 'http://loinc.org',
|
||||
'code' => '41988-7',
|
||||
'display' => 'Medical statement',
|
||||
]],
|
||||
'coding' => [
|
||||
[
|
||||
'system' => 'http://snomed.info/sct',
|
||||
'code' => '419891008',
|
||||
'display' => 'Record artifact',
|
||||
],
|
||||
[
|
||||
'system' => 'http://loinc.org',
|
||||
'code' => '41988-7',
|
||||
'display' => 'Medical statement',
|
||||
],
|
||||
],
|
||||
'text' => $docTitle,
|
||||
],
|
||||
'subject' => ['reference' => $patientRef],
|
||||
'author' => [[
|
||||
'reference' => $authorRef,
|
||||
'display' => $drName,
|
||||
]],
|
||||
'custodian' => ['reference' => 'urn:uuid:organization-' . ($orgId !== '' ? $orgId : 'IN0510000871')],
|
||||
'date' => $timestamp,
|
||||
'description' => $docTitle,
|
||||
'description' => $docTitle . ' PDF',
|
||||
'content' => [[
|
||||
'attachment' => [
|
||||
'contentType' => $contentType,
|
||||
'contentType' => 'application/pdf',
|
||||
'language' => 'en-IN',
|
||||
'data' => $docData,
|
||||
'title' => $docTitle,
|
||||
'size' => $fileSize,
|
||||
'title' => $docTitle . ' PDF',
|
||||
'creation' => $timestamp,
|
||||
],
|
||||
]],
|
||||
]);
|
||||
];
|
||||
|
||||
if ($sha1Hash !== '') {
|
||||
$docRef['content'][0]['attachment']['hash'] = $sha1Hash;
|
||||
}
|
||||
|
||||
$builder->addDocumentReference($docRef);
|
||||
|
||||
$bundle = $builder->toBundle();
|
||||
$validation = $this->validator->validate($bundle, 'health_document', [
|
||||
|
|
@ -122,4 +178,54 @@ class HealthDocumentFhirGenerator extends AbstractModuleFhirGenerator
|
|||
'validation' => $validation->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
private function convertToPdfBytes(string $rawBytes, string $contentType, string $docTitle): string
|
||||
{
|
||||
if ($rawBytes === '') {
|
||||
$rawBytes = '<div xmlns="http://www.w3.org/1999/xhtml"><h3>' . htmlspecialchars($docTitle, ENT_QUOTES, 'UTF-8') . '</h3><p>Health Document Summary</p></div>';
|
||||
}
|
||||
|
||||
if (str_starts_with($rawBytes, '%PDF')) {
|
||||
return $rawBytes;
|
||||
}
|
||||
|
||||
try {
|
||||
$mpdfTempDir = defined('WRITEPATH') ? WRITEPATH . 'cache' . DIRECTORY_SEPARATOR . 'mpdf' : sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'mpdf';
|
||||
if (! is_dir($mpdfTempDir)) {
|
||||
@mkdir($mpdfTempDir, 0755, true);
|
||||
}
|
||||
|
||||
ini_set('pcre.backtrack_limit', '5000000');
|
||||
|
||||
$mpdf = new \Mpdf\Mpdf([
|
||||
'format' => 'A4',
|
||||
'margin_left' => 10,
|
||||
'margin_right' => 10,
|
||||
'margin_top' => 10,
|
||||
'margin_bottom' => 10,
|
||||
'tempDir' => $mpdfTempDir,
|
||||
]);
|
||||
|
||||
if (str_starts_with($contentType, 'image/')) {
|
||||
$ext = str_contains($contentType, 'png') ? 'png' : 'jpg';
|
||||
$tempImg = $mpdfTempDir . DIRECTORY_SEPARATOR . 'img_' . md5($rawBytes) . '.' . $ext;
|
||||
@file_put_contents($tempImg, $rawBytes);
|
||||
|
||||
$html = '<!DOCTYPE html><html><body style="margin:0;padding:0;text-align:center;">'
|
||||
. '<h3>' . htmlspecialchars($docTitle, ENT_QUOTES, 'UTF-8') . '</h3>'
|
||||
. '<img src="' . $tempImg . '" style="max-width:100%; height:auto;" />'
|
||||
. '</body></html>';
|
||||
$mpdf->WriteHTML($html);
|
||||
@unlink($tempImg);
|
||||
} else {
|
||||
$html = str_contains($rawBytes, '<') ? $rawBytes : ('<!DOCTYPE html><html><body><h3>' . htmlspecialchars($docTitle, ENT_QUOTES, 'UTF-8') . '</h3><p>' . nl2br(htmlspecialchars($rawBytes, ENT_QUOTES, 'UTF-8')) . '</p></body></html>');
|
||||
$mpdf->WriteHTML($html);
|
||||
}
|
||||
|
||||
$pdfBytes = $mpdf->Output('', 'S');
|
||||
return is_string($pdfBytes) && str_starts_with($pdfBytes, '%PDF') ? $pdfBytes : '';
|
||||
} catch (\Throwable $e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,13 +16,32 @@ class WellnessFhirGenerator extends AbstractModuleFhirGenerator
|
|||
$sessionId = (string) ($source['session_id'] ?? '0');
|
||||
$visitDate = (string) ($source['visit_date'] ?? date('Y-m-d'));
|
||||
|
||||
$careContextReference = 'WELLNESS-' . $recordId . '-S' . $sessionId . '-' . $visitDate;
|
||||
$careContextDisplay = 'Wellness & Lifestyle Record ' . $visitDate;
|
||||
$careContextReference = 'WELLNESS-' . $recordId . ($sessionId !== '0' ? '-S' . $sessionId : '') . '-' . $visitDate . '-' . date('His');
|
||||
$careContextDisplay = 'Wellness & Vitals Record ' . $visitDate;
|
||||
|
||||
$builder = new \App\Libraries\Abdm\Fhir\FhirDocumentBuilder();
|
||||
|
||||
$composition = $this->buildBaseComposition($source, 'Wellness Record', '94500-6', 'Health and Wellness Record');
|
||||
$composition['meta']['profile'] = ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/WellnessRecord'];
|
||||
$composition['type'] = [
|
||||
'coding' => [
|
||||
[
|
||||
'system' => 'http://snomed.info/sct',
|
||||
'code' => '419891008',
|
||||
'display' => 'Record artifact',
|
||||
],
|
||||
],
|
||||
'text' => 'Wellness Record',
|
||||
];
|
||||
|
||||
$orgId = (string) ($source['organization']['id'] ?? $source['hfr_id'] ?? '1');
|
||||
$composition['custodian'] = ['reference' => 'urn:uuid:organization-' . ($orgId !== '' ? $orgId : '1')];
|
||||
|
||||
$patientRef = 'urn:uuid:patient-' . $patientId;
|
||||
|
||||
$builder
|
||||
->buildBundleMeta('wellness-' . $recordId . '-' . strtotime($timestamp), $timestamp)
|
||||
->buildComposition($this->buildBaseComposition($source, 'Wellness Record', '72198-1', 'Health record document'))
|
||||
->updateBundleMeta(['meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/WellnessBundle']]])
|
||||
->addPatient($this->buildBasePatient($source));
|
||||
|
||||
$encounter = $this->buildEncounter($source);
|
||||
|
|
@ -40,21 +59,74 @@ class WellnessFhirGenerator extends AbstractModuleFhirGenerator
|
|||
$builder->addOrganization($organization);
|
||||
}
|
||||
|
||||
$patientRef = 'urn:uuid:patient-' . $patientId;
|
||||
$encounterRef = is_array($encounter) ? 'urn:uuid:' . (string) ($encounter['id'] ?? '') : null;
|
||||
|
||||
// Vitals (Pulse, SpO2, BP, Temp, Height, Weight)
|
||||
foreach ((array) ($source['vitals'] ?? []) as $idx => $vital) {
|
||||
$vitalObsRefs = [];
|
||||
$examObsRefs = [];
|
||||
$womenObsRefs = [];
|
||||
$wellnessObsRefs = [];
|
||||
|
||||
// 1. Process Vital Signs
|
||||
$vitalsList = (array) ($source['vitals'] ?? []);
|
||||
$sysBp = null;
|
||||
$diaBp = null;
|
||||
$standaloneVitals = [];
|
||||
|
||||
foreach ($vitalsList as $vital) {
|
||||
$loincCode = (string) ($vital['loinc_code'] ?? $vital['code'] ?? '');
|
||||
$display = trim((string) ($vital['display'] ?? ''));
|
||||
if (! $this->isMeaningfulValue($display)) {
|
||||
continue;
|
||||
if ($loincCode === '8480-6' || strcasecmp($display, 'Systolic blood pressure') === 0) {
|
||||
$sysBp = $vital;
|
||||
} elseif ($loincCode === '8462-4' || strcasecmp($display, 'Diastolic blood pressure') === 0) {
|
||||
$diaBp = $vital;
|
||||
} else {
|
||||
$standaloneVitals[] = $vital;
|
||||
}
|
||||
}
|
||||
|
||||
if ($sysBp !== null || $diaBp !== null) {
|
||||
$obsId = 'obs-bp-' . $recordId;
|
||||
$vitalObsRefs[] = ['reference' => 'urn:uuid:' . $obsId];
|
||||
|
||||
$bpComponents = [];
|
||||
if ($sysBp !== null && is_numeric($sysBp['value'] ?? null)) {
|
||||
$bpComponents[] = [
|
||||
'code' => [
|
||||
'coding' => [[
|
||||
'system' => 'http://loinc.org',
|
||||
'code' => '8480-6',
|
||||
'display' => 'Systolic blood pressure',
|
||||
]],
|
||||
],
|
||||
'valueQuantity' => [
|
||||
'value' => (float) $sysBp['value'],
|
||||
'unit' => 'mmHg',
|
||||
'system' => 'http://unitsofmeasure.org',
|
||||
'code' => 'mm[Hg]',
|
||||
],
|
||||
];
|
||||
}
|
||||
if ($diaBp !== null && is_numeric($diaBp['value'] ?? null)) {
|
||||
$bpComponents[] = [
|
||||
'code' => [
|
||||
'coding' => [[
|
||||
'system' => 'http://loinc.org',
|
||||
'code' => '8462-4',
|
||||
'display' => 'Diastolic blood pressure',
|
||||
]],
|
||||
],
|
||||
'valueQuantity' => [
|
||||
'value' => (float) $diaBp['value'],
|
||||
'unit' => 'mmHg',
|
||||
'system' => 'http://unitsofmeasure.org',
|
||||
'code' => 'mm[Hg]',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$loinc = $this->codingResolver->resolveLoincForLabTest((string) ($vital['code'] ?? ''), $display);
|
||||
$ucum = $this->codingResolver->resolveUnitUcUM((string) ($vital['unit'] ?? ''));
|
||||
$builder->addObservation([
|
||||
$bpObs = [
|
||||
'resourceType' => 'Observation',
|
||||
'id' => 'vital-' . $recordId . '-' . $idx,
|
||||
'id' => $obsId,
|
||||
'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/ObservationVitalSigns']],
|
||||
'status' => 'final',
|
||||
'category' => [[
|
||||
|
|
@ -65,32 +137,125 @@ class WellnessFhirGenerator extends AbstractModuleFhirGenerator
|
|||
]],
|
||||
]],
|
||||
'code' => [
|
||||
'coding' => $loinc['coding'] ?? [],
|
||||
'coding' => [[
|
||||
'system' => 'http://loinc.org',
|
||||
'code' => '85354-9',
|
||||
'display' => 'Blood pressure panel',
|
||||
]],
|
||||
'text' => 'Blood Pressure',
|
||||
],
|
||||
'subject' => ['reference' => $patientRef],
|
||||
'effectiveDateTime' => $timestamp,
|
||||
];
|
||||
|
||||
if ($encounterRef) {
|
||||
$bpObs['encounter'] = ['reference' => $encounterRef];
|
||||
}
|
||||
if (! empty($bpComponents)) {
|
||||
$bpObs['component'] = $bpComponents;
|
||||
}
|
||||
|
||||
$builder->addObservation($bpObs);
|
||||
}
|
||||
|
||||
foreach ($standaloneVitals as $idx => $vital) {
|
||||
$display = trim((string) ($vital['display'] ?? ''));
|
||||
$val = $vital['value'] ?? null;
|
||||
if ($val === null || trim((string) $val) === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$loincCode = (string) ($vital['loinc_code'] ?? $vital['code'] ?? '');
|
||||
$unit = (string) ($vital['unit'] ?? '');
|
||||
$ucumCode = (string) ($vital['ucum_code'] ?? $vital['ucum'] ?? '');
|
||||
|
||||
$loinc = $this->codingResolver->resolveLoincForLabTest($loincCode, $display);
|
||||
$ucum = $this->codingResolver->resolveUnitUcUM($ucumCode ?: $unit);
|
||||
|
||||
$obsId = 'vital-' . $recordId . '-' . $idx;
|
||||
$vitalObsRefs[] = ['reference' => 'urn:uuid:' . $obsId];
|
||||
|
||||
$builder->addObservation([
|
||||
'resourceType' => 'Observation',
|
||||
'id' => $obsId,
|
||||
'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/ObservationVitalSigns']],
|
||||
'status' => 'final',
|
||||
'category' => [[
|
||||
'coding' => [[
|
||||
'system' => 'http://terminology.hl7.org/CodeSystem/observation-category',
|
||||
'code' => 'vital-signs',
|
||||
'display' => 'Vital Signs',
|
||||
]],
|
||||
]],
|
||||
'code' => [
|
||||
'coding' => $loinc['coding'] ?? [[
|
||||
'system' => 'http://loinc.org',
|
||||
'code' => $loincCode ?: '8716-3',
|
||||
'display' => $display,
|
||||
]],
|
||||
'text' => $display,
|
||||
],
|
||||
'subject' => ['reference' => $patientRef],
|
||||
'encounter' => $encounterRef ? ['reference' => $encounterRef] : null,
|
||||
'effectiveDateTime' => $timestamp,
|
||||
'valueQuantity' => [
|
||||
'value' => (float) ($vital['value'] ?? 0),
|
||||
'unit' => (string) ($vital['unit'] ?? ''),
|
||||
'value' => is_numeric($val) ? (float) $val : (string) $val,
|
||||
'unit' => $unit,
|
||||
'system' => 'http://unitsofmeasure.org',
|
||||
'code' => (string) ($ucum['code'] ?? ''),
|
||||
'code' => (string) ($ucum['code'] ?? $ucumCode ?: $unit),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
// General Advice / Diet & Lifestyle Observations
|
||||
$adviceItems = (array) ($source['advice'] ?? $source['general_advice'] ?? []);
|
||||
foreach ($adviceItems as $idx => $adviceText) {
|
||||
$text = trim((string) (is_array($adviceText) ? ($adviceText['text'] ?? '') : $adviceText));
|
||||
// 2. Process Physical Examination Findings
|
||||
$examList = (array) ($source['physical_examination'] ?? $source['findings'] ?? []);
|
||||
foreach ($examList as $idx => $examItem) {
|
||||
$text = trim((string) (is_array($examItem) ? ($examItem['text'] ?? $examItem['display'] ?? '') : $examItem));
|
||||
if (! $this->isMeaningfulValue($text)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$obsId = 'exam-' . $recordId . '-' . $idx;
|
||||
$examObsRefs[] = ['reference' => 'urn:uuid:' . $obsId];
|
||||
|
||||
$builder->addObservation([
|
||||
'resourceType' => 'Observation',
|
||||
'id' => 'advice-' . $recordId . '-' . $idx,
|
||||
'id' => $obsId,
|
||||
'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Observation']],
|
||||
'status' => 'final',
|
||||
'category' => [[
|
||||
'coding' => [[
|
||||
'system' => 'http://terminology.hl7.org/CodeSystem/observation-category',
|
||||
'code' => 'exam',
|
||||
'display' => 'Exam',
|
||||
]],
|
||||
]],
|
||||
'code' => [
|
||||
'coding' => [[
|
||||
'system' => 'http://loinc.org',
|
||||
'code' => '29545-1',
|
||||
'display' => 'Physical findings',
|
||||
]],
|
||||
'text' => 'Physical Examination',
|
||||
],
|
||||
'subject' => ['reference' => $patientRef],
|
||||
'encounter' => $encounterRef ? ['reference' => $encounterRef] : null,
|
||||
'effectiveDateTime' => $timestamp,
|
||||
'valueString' => $text,
|
||||
]);
|
||||
}
|
||||
|
||||
// 3. Process Women Wellness / Reproductive Health (LMP, Gravida, Para, etc.)
|
||||
$womenWellness = (array) ($source['women_wellness'] ?? []);
|
||||
$lmpDate = trim((string) ($womenWellness['lmp'] ?? $source['lmp'] ?? ''));
|
||||
if ($lmpDate !== '') {
|
||||
$obsId = 'women-lmp-' . $recordId;
|
||||
$womenObsRefs[] = ['reference' => 'urn:uuid:' . $obsId];
|
||||
|
||||
$builder->addObservation([
|
||||
'resourceType' => 'Observation',
|
||||
'id' => $obsId,
|
||||
'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Observation']],
|
||||
'status' => 'final',
|
||||
'category' => [[
|
||||
'coding' => [[
|
||||
|
|
@ -100,7 +265,80 @@ class WellnessFhirGenerator extends AbstractModuleFhirGenerator
|
|||
]],
|
||||
]],
|
||||
'code' => [
|
||||
'text' => 'Lifestyle & Diet Advice',
|
||||
'coding' => [[
|
||||
'system' => 'http://loinc.org',
|
||||
'code' => '8665-2',
|
||||
'display' => 'Last menstrual period start date',
|
||||
]],
|
||||
'text' => 'Last Menstrual Period (LMP)',
|
||||
],
|
||||
'subject' => ['reference' => $patientRef],
|
||||
'encounter' => $encounterRef ? ['reference' => $encounterRef] : null,
|
||||
'effectiveDateTime' => $timestamp,
|
||||
'valueDateTime' => date(DATE_ATOM, strtotime($lmpDate) ?: time()),
|
||||
]);
|
||||
}
|
||||
|
||||
foreach ($womenWellness as $key => $valStr) {
|
||||
if ($key === 'lmp' || ! is_string($valStr) || trim($valStr) === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$obsId = 'women-obs-' . $recordId . '-' . $key;
|
||||
$womenObsRefs[] = ['reference' => 'urn:uuid:' . $obsId];
|
||||
|
||||
$builder->addObservation([
|
||||
'resourceType' => 'Observation',
|
||||
'id' => $obsId,
|
||||
'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Observation']],
|
||||
'status' => 'final',
|
||||
'category' => [[
|
||||
'coding' => [[
|
||||
'system' => 'http://terminology.hl7.org/CodeSystem/observation-category',
|
||||
'code' => 'social-history',
|
||||
'display' => 'Social History',
|
||||
]],
|
||||
]],
|
||||
'code' => [
|
||||
'text' => ucwords(str_replace('_', ' ', $key)),
|
||||
],
|
||||
'subject' => ['reference' => $patientRef],
|
||||
'encounter' => $encounterRef ? ['reference' => $encounterRef] : null,
|
||||
'effectiveDateTime' => $timestamp,
|
||||
'valueString' => trim($valStr),
|
||||
]);
|
||||
}
|
||||
|
||||
// 4. Process General Wellness / Lifestyle & Diet Advice
|
||||
$adviceItems = (array) ($source['advice'] ?? $source['general_advice'] ?? $source['lifestyle'] ?? []);
|
||||
foreach ($adviceItems as $idx => $adviceText) {
|
||||
$text = trim((string) (is_array($adviceText) ? ($adviceText['text'] ?? $adviceText['display'] ?? '') : $adviceText));
|
||||
if (! $this->isMeaningfulValue($text)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$obsId = 'wellness-social-' . $recordId . '-' . $idx;
|
||||
$wellnessObsRefs[] = ['reference' => 'urn:uuid:' . $obsId];
|
||||
|
||||
$builder->addObservation([
|
||||
'resourceType' => 'Observation',
|
||||
'id' => $obsId,
|
||||
'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Observation']],
|
||||
'status' => 'final',
|
||||
'category' => [[
|
||||
'coding' => [[
|
||||
'system' => 'http://terminology.hl7.org/CodeSystem/observation-category',
|
||||
'code' => 'social-history',
|
||||
'display' => 'Social History',
|
||||
]],
|
||||
]],
|
||||
'code' => [
|
||||
'coding' => [[
|
||||
'system' => 'http://loinc.org',
|
||||
'code' => '8670-2',
|
||||
'display' => 'History of Social history',
|
||||
]],
|
||||
'text' => 'Lifestyle & General Wellness Advice',
|
||||
],
|
||||
'subject' => ['reference' => $patientRef],
|
||||
'encounter' => $encounterRef ? ['reference' => $encounterRef] : null,
|
||||
|
|
@ -109,6 +347,70 @@ class WellnessFhirGenerator extends AbstractModuleFhirGenerator
|
|||
]);
|
||||
}
|
||||
|
||||
// Build Composition Sections strictly adhering to NRCES ABDM FHIR R4 Implementation Guide (v6.5.0)
|
||||
$sections = [];
|
||||
if (! empty($vitalObsRefs)) {
|
||||
$sections[] = [
|
||||
'title' => 'Vital Signs',
|
||||
'code' => [
|
||||
'coding' => [[
|
||||
'system' => 'http://loinc.org',
|
||||
'code' => '8716-3',
|
||||
'display' => 'Vital signs',
|
||||
]],
|
||||
],
|
||||
'entry' => $vitalObsRefs,
|
||||
];
|
||||
}
|
||||
|
||||
if (! empty($examObsRefs)) {
|
||||
$sections[] = [
|
||||
'title' => 'General Assessment',
|
||||
'entry' => $examObsRefs,
|
||||
];
|
||||
}
|
||||
|
||||
if (! empty($womenObsRefs)) {
|
||||
$sections[] = [
|
||||
'title' => 'Women Health',
|
||||
'entry' => $womenObsRefs,
|
||||
];
|
||||
}
|
||||
|
||||
if (! empty($wellnessObsRefs)) {
|
||||
$sections[] = [
|
||||
'title' => 'Lifestyle',
|
||||
'entry' => $wellnessObsRefs,
|
||||
];
|
||||
}
|
||||
|
||||
if (empty($sections)) {
|
||||
$dummyObsId = 'wellness-note-' . $recordId;
|
||||
$builder->addObservation([
|
||||
'resourceType' => 'Observation',
|
||||
'id' => $dummyObsId,
|
||||
'status' => 'final',
|
||||
'code' => ['text' => 'General Wellness Note'],
|
||||
'subject' => ['reference' => $patientRef, 'display' => (string) ($source['patient']['name'] ?? 'Patient')],
|
||||
'effectiveDateTime' => $timestamp,
|
||||
'valueString' => 'General Wellness & Vitals Assessment for ' . $visitDate,
|
||||
]);
|
||||
$sections[] = [
|
||||
'title' => 'Vital Signs',
|
||||
'code' => [
|
||||
'coding' => [[
|
||||
'system' => 'http://loinc.org',
|
||||
'code' => '8716-3',
|
||||
'display' => 'Vital signs',
|
||||
]],
|
||||
],
|
||||
'entry' => [['reference' => 'urn:uuid:' . $dummyObsId]],
|
||||
];
|
||||
}
|
||||
|
||||
$composition['section'] = $sections;
|
||||
$builder->buildComposition($composition);
|
||||
|
||||
$bundle = $builder->toBundle();
|
||||
$validation = $this->validator->validate($bundle, 'wellness', ['resolved' => 1, 'unresolved' => 0, 'fallback_used' => 0]);
|
||||
|
||||
|
|
@ -121,3 +423,4 @@ class WellnessFhirGenerator extends AbstractModuleFhirGenerator
|
|||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -763,6 +763,7 @@ class BridgeSyncService
|
|||
'abdm.consent.requested' => '/api/v1/abdm/consents/request',
|
||||
'abdm.fhir.share.requested' => '/api/v1/abdm/health-information/share',
|
||||
'abdm.opd.prescription.share.requested' => '/api/v1/abdm/health-information/share',
|
||||
'abdm.health_document.share.requested' => '/api/v1/abdm/health-information/share',
|
||||
'abdm.ipd.admission.share.requested' => '/api/v1/abdm/health-information/ipd-admission/share',
|
||||
'abdm.ipd.discharge.share.requested' => '/api/v1/abdm/health-information/ipd-discharge/share',
|
||||
'abdm.diagnosis.report.share.requested' => '/api/v1/abdm/health-information/diagnosis-report/share',
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
<th>Mobile</th>
|
||||
<th>Type</th>
|
||||
<th>Amount</th>
|
||||
<th>Balance</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
|
@ -32,6 +33,7 @@
|
|||
<?php
|
||||
$invType = ((int) ($row->insurance_id ?? 0) > 1) ? 'Org.' : 'Direct';
|
||||
$invDate = ! empty($row->inv_date) ? date('d-m-Y', strtotime((string) $row->inv_date)) : '-';
|
||||
$balance = (float) ($row->payment_part_balance ?? 0);
|
||||
?>
|
||||
<tr>
|
||||
<td><?= esc((string) ($row->invoice_code ?? '-')) ?></td>
|
||||
|
|
@ -41,6 +43,13 @@
|
|||
<td><?= esc((string) ($row->mphone1 ?? '-')) ?></td>
|
||||
<td><?= esc($invType) ?></td>
|
||||
<td><?= esc(number_format((float) ($row->net_amount ?? 0), 2, '.', '')) ?></td>
|
||||
<td>
|
||||
<?php if ($balance > 0): ?>
|
||||
<span class="badge bg-danger-subtle text-danger font-monospace fs-6"><?= esc(number_format($balance, 2, '.', '')) ?></span>
|
||||
<?php else: ?>
|
||||
<span class="text-muted font-monospace"><?= esc(number_format($balance, 2, '.', '')) ?></span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" onclick="navigateOrLoad('<?= base_url('billing/charges/show') ?>/<?= (int) ($row->inv_id ?? 0) ?>','Charges Invoice');">Open</button>
|
||||
</td>
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@
|
|||
<button type="button" class="btn btn-sm btn-outline-primary filter-btn" data-filter="immunization_record_publish">Immunization Record</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary filter-btn" data-filter="invoice">Invoice</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary filter-btn" data-filter="health_document_publish">Health Document</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary filter-btn" data-filter="wellness_record_publish">Wellness Record</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary filter-btn" data-filter="ipd_discharge_publish">IPD Discharge</button>
|
||||
</div>
|
||||
|
||||
|
|
@ -124,7 +125,7 @@
|
|||
$opdSessionId = (int) ($meta['opd_session_id'] ?? 0);
|
||||
$showSandbox = ($type === 'opd_prescription_publish' && $opdId > 0);
|
||||
$showPreview = ($type === 'opd_prescription_publish' && $opdId > 0)
|
||||
|| in_array($type, ['lab_report_publish', 'radiology_report_publish', 'ipd_discharge_publish', 'immunization_record_publish', 'health_document_publish'], true);
|
||||
|| in_array($type, ['lab_report_publish', 'radiology_report_publish', 'ipd_discharge_publish', 'immunization_record_publish', 'health_document_publish', 'wellness_record_publish'], true);
|
||||
$bridgeSubmitted = (int) ($t['bridge_submitted'] ?? 0) === 1;
|
||||
$bridgeCareContext = trim((string) ($t['bridge_care_context_reference'] ?? ''));
|
||||
?>
|
||||
|
|
@ -1121,6 +1122,17 @@
|
|||
|
||||
previewUrl = '<?= base_url('DoctorDocument/health_document_fhir_preview') ?>/' + healthDocId;
|
||||
title = 'HealthDocumentRecord FHIR Preview — Document #' + healthDocId;
|
||||
} else if (taskType === 'wellness_record_publish') {
|
||||
var wellnessPatientId = parseInt(row.getAttribute('data-patient-id') || '0', 10) || 0;
|
||||
var wellnessOpdId = parseInt(row.getAttribute('data-entity-id') || '0', 10) || 0;
|
||||
|
||||
if (wellnessPatientId <= 0) {
|
||||
setStatus('Task data missing for Wellness Record FHIR preview.', true);
|
||||
return true;
|
||||
}
|
||||
|
||||
previewUrl = '<?= base_url('DoctorDocument/wellness_record_fhir_preview') ?>/' + wellnessPatientId + (wellnessOpdId > 0 ? ('/' + wellnessOpdId) : '');
|
||||
title = 'WellnessRecord FHIR Preview — Patient #' + wellnessPatientId;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -2668,19 +2680,10 @@
|
|||
}
|
||||
|
||||
if (btnRegenerateFhirModal) {
|
||||
var taskType = _fhirSubmitTaskRow ? (_fhirSubmitTaskRow.getAttribute('data-task-type') || '').toLowerCase() : '';
|
||||
var canRebuildDiagnostic = _fhirSubmitMode === 'task'
|
||||
&& (taskType === 'lab_report_publish' || taskType === 'radiology_report_publish');
|
||||
btnRegenerateFhirModal.disabled = false;
|
||||
btnRegenerateFhirModal.style.display = _fhirSubmitMode === 'task' && !canRebuildDiagnostic ? 'none' : '';
|
||||
btnRegenerateFhirModal.innerHTML = _fhirSubmitMode === 'invoice' || canRebuildDiagnostic
|
||||
? '<i class="bi bi-arrow-clockwise"></i> Rebuild Digital Record'
|
||||
: '<i class="bi bi-arrow-clockwise"></i> Regenerate';
|
||||
btnRegenerateFhirModal.title = canRebuildDiagnostic
|
||||
? 'Rebuild FHIR and Digital Share PDF from the latest report data'
|
||||
: (_fhirSubmitMode === 'invoice'
|
||||
? 'Rebuild FHIR and Digital Share PDF from current billing data'
|
||||
: 'Rebuild FHIR bundle from current prescription data');
|
||||
btnRegenerateFhirModal.style.display = '';
|
||||
btnRegenerateFhirModal.innerHTML = '<i class="bi bi-arrow-clockwise"></i> Recreate FHIR Record';
|
||||
btnRegenerateFhirModal.title = 'Re-generate FHIR bundle fresh from current clinical and patient data';
|
||||
}
|
||||
|
||||
fhirPreviewModal.show();
|
||||
|
|
@ -2690,7 +2693,7 @@
|
|||
.then(function (res) {
|
||||
var parsed = null;
|
||||
try { parsed = JSON.parse(res.body || '{}'); } catch (e) { parsed = { raw: res.body || '' }; }
|
||||
var bundle = (parsed && parsed.resourceType === 'Bundle') ? parsed : ((parsed && parsed.bundle) ? parsed.bundle : null);
|
||||
var bundle = (parsed && parsed.resourceType === 'Bundle') ? parsed : ((parsed && parsed.bundle) ? parsed.bundle : ((parsed && parsed.fhir_bundle) ? parsed.fhir_bundle : null));
|
||||
var hasFhir = bundle !== null;
|
||||
_fhirOpdId = parseInt(parsed.opd_id || '0', 10) || 0;
|
||||
_fhirSessionId = parseInt(parsed.opd_session_id || '0', 10) || 0;
|
||||
|
|
@ -2714,71 +2717,21 @@
|
|||
});
|
||||
}
|
||||
|
||||
// Regenerate FHIR bundle in modal
|
||||
// Regenerate / Recreate FHIR bundle in modal
|
||||
btnRegenerateFhirModal.addEventListener('click', function () {
|
||||
var btn = this;
|
||||
var taskType = _fhirSubmitTaskRow ? (_fhirSubmitTaskRow.getAttribute('data-task-type') || '').toLowerCase() : '';
|
||||
if (_fhirSubmitMode === 'task' && (taskType === 'lab_report_publish' || taskType === 'radiology_report_publish')) {
|
||||
if (_fhirPreviewUrl === '') {
|
||||
alert('Diagnostic preview URL is unavailable.');
|
||||
return;
|
||||
}
|
||||
if (!confirm('Rebuild this FHIR record and Digital Share PDF from the latest report data?')) return;
|
||||
openFhirModal(_fhirPreviewUrl, fhirModalTitle ? fhirModalTitle.textContent : 'Diagnostic FHIR Preview', {
|
||||
submitMode: 'task',
|
||||
taskRow: _fhirSubmitTaskRow,
|
||||
abhaId: _fhirSubmitAbha
|
||||
});
|
||||
if (!_fhirPreviewUrl) {
|
||||
alert('No FHIR preview URL available for this record.');
|
||||
return;
|
||||
}
|
||||
if (_fhirSubmitMode === 'invoice') {
|
||||
if (!_fhirInvoiceSource || _fhirInvoiceBillId <= 0 || _fhirInvoicePatientId <= 0) {
|
||||
alert('Invoice source data is incomplete.');
|
||||
return;
|
||||
}
|
||||
if (!confirm('Rebuild this FHIR InvoiceRecord from current billing data?')) return;
|
||||
var invoiceUrl = '<?= base_url('AbdmGateway/invoice_fhir_preview') ?>'
|
||||
+ '?source=' + encodeURIComponent(_fhirInvoiceSource)
|
||||
+ '&bill_id=' + encodeURIComponent(String(_fhirInvoiceBillId))
|
||||
+ '&patient_id=' + encodeURIComponent(String(_fhirInvoicePatientId));
|
||||
openFhirModal(invoiceUrl, fhirModalTitle ? fhirModalTitle.textContent : 'FHIR Invoice Preview', {
|
||||
submitMode: 'invoice',
|
||||
taskRow: _fhirSubmitTaskRow,
|
||||
invoiceSource: _fhirInvoiceSource,
|
||||
invoiceBillId: _fhirInvoiceBillId,
|
||||
invoicePatientId: _fhirInvoicePatientId
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (_fhirOpdId <= 0) { alert('No FHIR bundle loaded.'); return; }
|
||||
if (!confirm('Rebuild FHIR bundle for OPD #' + _fhirOpdId + '?\nThis overwrites the stored bundle with fresh data.')) return;
|
||||
var origHtml = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Rebuilding…';
|
||||
var body = new URLSearchParams({ opd_id: _fhirOpdId, opd_session_id: _fhirSessionId });
|
||||
body.append(csrfName, csrfHash);
|
||||
fetch('<?= base_url('Opd_prescription/fhir_bundle_regenerate') ?>', {
|
||||
method: 'POST',
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: body.toString()
|
||||
})
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (res) {
|
||||
if (res.csrfName && res.csrfHash) { csrfName = res.csrfName; csrfHash = res.csrfHash; }
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = origHtml;
|
||||
if (res.ok === 1 || res.ok === '1') {
|
||||
// Reload modal with fresh bundle
|
||||
var reloadUrl = '<?= base_url('Opd_prescription/fhir_bundle_preview') ?>/' + _fhirOpdId + (_fhirSessionId > 0 ? '/' + _fhirSessionId : '');
|
||||
openFhirModal(reloadUrl, fhirModalTitle ? fhirModalTitle.textContent : 'FHIR Preview');
|
||||
} else {
|
||||
alert('\u274c Regeneration failed: ' + (res.message || 'unknown error'));
|
||||
}
|
||||
})
|
||||
.catch(function (e) {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = origHtml;
|
||||
alert('\u274c ' + e.message);
|
||||
if (!confirm('Recreate FHIR Record fresh from latest clinical and patient data?')) return;
|
||||
var freshUrl = _fhirPreviewUrl + (_fhirPreviewUrl.indexOf('?') !== -1 ? '&' : '?') + 'force_rebuild=1&refresh=' + Date.now();
|
||||
openFhirModal(freshUrl, fhirModalTitle ? fhirModalTitle.textContent : 'FHIR Preview', {
|
||||
submitMode: _fhirSubmitMode,
|
||||
taskRow: _fhirSubmitTaskRow,
|
||||
abhaId: _fhirSubmitAbha,
|
||||
invoiceSource: _fhirInvoiceSource,
|
||||
invoiceBillId: _fhirInvoiceBillId,
|
||||
invoicePatientId: _fhirInvoicePatientId
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -2995,6 +2948,66 @@
|
|||
return;
|
||||
}
|
||||
|
||||
if (taskType === 'wellness_record_publish') {
|
||||
var wellnessOpdId = parseInt(_fhirSubmitTaskRow.getAttribute('data-entity-id') || '0', 10) || 0;
|
||||
var wellnessPatientId = parseInt(_fhirSubmitTaskRow.getAttribute('data-patient-id') || '0', 10) || 0;
|
||||
if (wellnessPatientId <= 0) {
|
||||
alert('Task data missing (patient_id).');
|
||||
return;
|
||||
}
|
||||
|
||||
post('<?= base_url('AbdmGateway/share_wellness_bundle') ?>', {
|
||||
task_id: parseInt(_fhirSubmitTaskRow.getAttribute('data-task-id') || '0', 10) || 0,
|
||||
patient_id: wellnessPatientId,
|
||||
opd_id: wellnessOpdId,
|
||||
abha_id: _fhirSubmitAbha,
|
||||
push_to_gateway: 1
|
||||
}, function (res) {
|
||||
if (!res || parseInt(res.ok || '0', 10) !== 1) {
|
||||
alert('ABDM Bridge push failed:\n' + ((res && (res.error_text || res.error || res.message)) || 'Unknown error'));
|
||||
return;
|
||||
}
|
||||
var badge = _fhirSubmitTaskRow.querySelector('.status-pill');
|
||||
if (badge) {
|
||||
badge.textContent = 'BRIDGE SUBMITTED';
|
||||
badge.className = 'badge bg-info status-pill';
|
||||
}
|
||||
setStatus('Submitted to ABDM Bridge. Queue ID: ' + (res.queue_id || '-') + '; care context: ' + (res.care_context_reference || '-'));
|
||||
fhirPreviewModal.hide();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (taskType === 'health_document_publish') {
|
||||
var healthDocId = parseInt(_fhirSubmitTaskRow.getAttribute('data-entity-id') || '0', 10) || 0;
|
||||
var healthDocPatientId = parseInt(_fhirSubmitTaskRow.getAttribute('data-patient-id') || '0', 10) || 0;
|
||||
if (healthDocPatientId <= 0 || healthDocId <= 0) {
|
||||
alert('Task data missing (record_id/patient_id).');
|
||||
return;
|
||||
}
|
||||
|
||||
post('<?= base_url('AbdmGateway/share_health_document_bundle') ?>', {
|
||||
task_id: parseInt(_fhirSubmitTaskRow.getAttribute('data-task-id') || '0', 10) || 0,
|
||||
patient_id: healthDocPatientId,
|
||||
record_id: healthDocId,
|
||||
abha_id: _fhirSubmitAbha,
|
||||
push_to_gateway: 1
|
||||
}, function (res) {
|
||||
if (!res || parseInt(res.ok || '0', 10) !== 1) {
|
||||
alert('ABDM Bridge push failed:\n' + ((res && (res.error_text || res.error || res.message)) || 'Unknown error'));
|
||||
return;
|
||||
}
|
||||
var badge = _fhirSubmitTaskRow.querySelector('.status-pill');
|
||||
if (badge) {
|
||||
badge.textContent = 'BRIDGE SUBMITTED';
|
||||
badge.className = 'badge bg-info status-pill';
|
||||
}
|
||||
setStatus('Submitted to ABDM Bridge. Queue ID: ' + (res.queue_id || '-') + '; care context: ' + (res.care_context_reference || '-'));
|
||||
fhirPreviewModal.hide();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
runAction(_fhirSubmitTaskRow, 'submit', _fhirSubmitAbha);
|
||||
fhirPreviewModal.hide();
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -229,13 +229,19 @@ if ($patientPhotoPath === '') {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 d-flex flex-wrap gap-2">
|
||||
<div class="mt-2 d-flex flex-wrap gap-2 align-items-center">
|
||||
<?php if ((int) ($group['rx_session_id'] ?? 0) > 0) { ?>
|
||||
<a class="btn btn-primary btn-sm" target="_blank"
|
||||
href="<?= base_url('Opd_prescription/opd_prescription_print/' . (int) $group['opd_id'] . '/' . (int) $group['rx_session_id']) ?>">
|
||||
Prescription Print
|
||||
</a>
|
||||
<?php } ?>
|
||||
<button type="button" class="btn btn-outline-info btn-sm" onclick="previewWellnessFhir(<?= (int) ($patient->id ?? 0) ?>, <?= (int) ($group['opd_id'] ?? 0) ?>)">
|
||||
<i class="bi bi-activity me-1"></i>Wellness Record FHIR Preview
|
||||
</button>
|
||||
<button type="button" class="btn btn-success btn-sm text-white" onclick="generateWellnessFhir(<?= (int) ($patient->id ?? 0) ?>, <?= (int) ($group['opd_id'] ?? 0) ?>)">
|
||||
<i class="bi bi-lightning-charge me-1"></i>Push ABDM Wellness
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($group['bp']) || !empty($group['diastolic']) || !empty($group['pulse']) || !empty($group['temp']) || !empty($group['spo2'])) { ?>
|
||||
|
|
@ -389,14 +395,19 @@ if ($patientPhotoPath === '') {
|
|||
<?php endif; ?>
|
||||
|
||||
<div class="tab-pane fade pt-3" id="opd-documents-tab" role="tabpanel">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
|
||||
<div>
|
||||
<h5 class="card-title mb-0 py-0">Patient Scanned & Uploaded Documents</h5>
|
||||
<div class="text-muted small">All documents attached to this patient ordered by date (Latest First)</div>
|
||||
<h5 class="card-title mb-0 py-0"><i class="bi bi-file-earmark-medical text-primary me-1"></i>Patient Scanned & Uploaded Documents</h5>
|
||||
<div class="text-muted small">Scanned copies, webcam captures, and uploaded reports formatted for ABDM HealthDocumentRecord</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<button type="button" class="btn btn-sm btn-success text-white shadow-sm fw-semibold" onclick="generateAllFhirDocuments(<?= (int) ($patient->id ?? 0) ?>)" id="btnGenerateAllFhirDocs">
|
||||
<i class="bi bi-diagram-3-fill me-1"></i>Generate ABDM FHIR Bundle (All)
|
||||
</button>
|
||||
<span class="badge bg-secondary-subtle text-secondary border border-secondary-subtle">
|
||||
Total Documents: <?= count($patientDocuments ?? []) ?>
|
||||
</span>
|
||||
</div>
|
||||
<span class="badge bg-secondary-subtle text-secondary border border-secondary-subtle">
|
||||
Total Documents: <?= count($patientDocuments ?? []) ?>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<?php if (empty($patientDocuments)) : ?>
|
||||
|
|
@ -406,15 +417,28 @@ if ($patientPhotoPath === '') {
|
|||
<?php else : ?>
|
||||
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-3">
|
||||
<?php foreach ($patientDocuments as $doc) : ?>
|
||||
<div class="col">
|
||||
<div class="col" id="doc-card-col-<?= (int) $doc['id'] ?>">
|
||||
<div class="card h-100 border shadow-sm">
|
||||
<div class="card-header py-2 d-flex justify-content-between align-items-center bg-light">
|
||||
<span class="fw-bold text-truncate" style="max-width: 180px;" title="<?= esc($doc['title']) ?>"><?= esc($doc['title']) ?></span>
|
||||
<?php if ($doc['isPdf']) : ?>
|
||||
<span class="badge bg-danger-subtle text-danger border border-danger-subtle"><i class="bi bi-file-earmark-pdf me-1"></i>PDF</span>
|
||||
<?php else : ?>
|
||||
<span class="badge bg-primary-subtle text-primary border border-primary-subtle"><i class="bi bi-file-earmark-image me-1"></i><?= esc(strtoupper($doc['ext'] ?: 'IMAGE')) ?></span>
|
||||
<?php endif; ?>
|
||||
<span class="fw-bold text-truncate" style="max-width: 170px;" title="<?= esc($doc['title']) ?>">
|
||||
<i class="bi bi-folder2-open me-1 text-primary"></i><?= esc($doc['title']) ?>
|
||||
</span>
|
||||
<div class="d-flex align-items-center gap-1">
|
||||
<?php if (! empty($doc['hasSync']) || ! empty($doc['hasTask'])) : ?>
|
||||
<span class="badge bg-success-subtle text-success border border-success-subtle" title="HealthDocumentRecord FHIR bundle created & enqueued for ABDM sync">
|
||||
<i class="bi bi-check-circle-fill me-1"></i>FHIR Ready
|
||||
</span>
|
||||
<?php else : ?>
|
||||
<span class="badge bg-info-subtle text-info border border-info-subtle" title="Document uploaded and ready for FHIR bundling">
|
||||
<i class="bi bi-file-earmark-code me-1"></i>FHIR Available
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
<?php if ($doc['isPdf']) : ?>
|
||||
<span class="badge bg-danger-subtle text-danger border border-danger-subtle"><i class="bi bi-file-earmark-pdf me-1"></i>PDF</span>
|
||||
<?php else : ?>
|
||||
<span class="badge bg-primary-subtle text-primary border border-primary-subtle"><i class="bi bi-file-earmark-image me-1"></i><?= esc(strtoupper($doc['ext'] ?: 'IMAGE')) ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-2 text-center bg-light-subtle d-flex align-items-center justify-content-center" style="height: 180px;">
|
||||
<?php if ($doc['isPdf']) : ?>
|
||||
|
|
@ -428,9 +452,19 @@ if ($patientPhotoPath === '') {
|
|||
</div>
|
||||
<div class="card-footer py-2 d-flex justify-content-between align-items-center bg-white text-muted small">
|
||||
<span><i class="bi bi-clock me-1"></i><?= esc($doc['insertDate'] ?: '-') ?></span>
|
||||
<a href="<?= base_url(ltrim($doc['path'], '/')) ?>" target="_blank" class="btn btn-sm btn-outline-primary py-0" style="font-size: 12px;">
|
||||
<i class="bi bi-box-arrow-up-right me-1"></i>Open
|
||||
</a>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<a href="<?= base_url(ltrim($doc['path'], '/')) ?>" target="_blank" class="btn btn-outline-secondary py-0" style="font-size: 11px;">
|
||||
<i class="bi bi-box-arrow-up-right me-1"></i>Open
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-info py-0" style="font-size: 11px;" onclick="previewDocumentFhir(<?= (int) $doc['id'] ?>, '<?= esc($doc['title'], 'js') ?>')">
|
||||
<i class="bi bi-eye me-1"></i>FHIR Preview
|
||||
</button>
|
||||
<?php if (empty($doc['hasSync']) && empty($doc['hasTask'])) : ?>
|
||||
<button type="button" class="btn btn-outline-success py-0" style="font-size: 11px;" onclick="generateSingleDocumentFhir(<?= (int) $doc['id'] ?>)" id="btn-fhir-single-<?= (int) $doc['id'] ?>">
|
||||
<i class="bi bi-lightning-charge me-1"></i>Push ABDM
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1860,3 +1894,196 @@ $(function() {
|
|||
});
|
||||
</script>
|
||||
|
||||
<!-- ABDM Scanned Document FHIR Preview Modal -->
|
||||
<div class="modal fade" id="patientDocFhirModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable">
|
||||
<div class="modal-content border-0 shadow-lg">
|
||||
<div class="modal-header bg-primary text-white py-2">
|
||||
<h6 class="modal-title mb-0 fw-bold" id="patientDocFhirTitle">
|
||||
<i class="bi bi-file-earmark-code me-2"></i>HealthDocumentRecord FHIR Bundle Preview
|
||||
</h6>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body p-0">
|
||||
<div class="bg-light border-bottom px-3 py-2 d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="badge bg-success" id="patientDocFhirBadge">HAS FHIR</span>
|
||||
<span class="badge bg-info text-dark" id="patientDocProfileBadge">HealthDocumentRecord</span>
|
||||
<small class="text-muted" id="patientDocFhirStatusText">Loaded (200)</small>
|
||||
</div>
|
||||
<ul class="nav nav-pills nav-sm" id="patientDocFhirTab" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active py-1 px-3 fs-7" id="patientDocFormTabBtn" data-bs-toggle="pill" data-bs-target="#patientDocFormTab" type="button" role="tab">Summary View</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link py-1 px-3 fs-7" id="patientDocJsonTabBtn" data-bs-toggle="pill" data-bs-target="#patientDocJsonTab" type="button" role="tab">JSON View</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tab-content p-3" id="patientDocFhirTabContent">
|
||||
<div class="tab-pane fade show active" id="patientDocFormTab" role="tabpanel">
|
||||
<div id="patientDocFormView" class="small">
|
||||
<div class="text-center text-muted py-4"><div class="spinner-border spinner-border-sm me-2"></div>Loading FHIR HealthDocumentRecord...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="patientDocJsonTab" role="tabpanel">
|
||||
<div class="d-flex justify-content-end mb-2">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="copyDocFhirJson()">
|
||||
<i class="bi bi-clipboard me-1"></i>Copy JSON
|
||||
</button>
|
||||
</div>
|
||||
<pre id="patientDocJsonView" class="bg-dark text-success p-3 rounded fs-7 mb-0" style="max-height: 500px; overflow-y: auto;"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer py-2 bg-light">
|
||||
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function copyDocFhirJson() {
|
||||
var jsonText = document.getElementById('patientDocJsonView').textContent;
|
||||
if (jsonText && navigator.clipboard) {
|
||||
navigator.clipboard.writeText(jsonText).then(function() {
|
||||
alert('FHIR JSON copied to clipboard!');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function previewDocumentFhir(docId, docTitle) {
|
||||
var modalEl = document.getElementById('patientDocFhirModal');
|
||||
var modal = bootstrap.Modal.getOrCreateInstance(modalEl);
|
||||
document.getElementById('patientDocFhirTitle').innerHTML = '<i class="bi bi-file-earmark-code me-2"></i>FHIR Preview - ' + (docTitle || ('Document #' + docId));
|
||||
document.getElementById('patientDocFormView').innerHTML = '<div class="text-center text-muted py-4"><div class="spinner-border spinner-border-sm text-primary me-2"></div>Building ABDM HealthDocumentRecord FHIR bundle...</div>';
|
||||
document.getElementById('patientDocJsonView').textContent = 'Loading...';
|
||||
modal.show();
|
||||
|
||||
fetch('<?= base_url('DoctorDocument/health_document_fhir_preview') ?>/' + docId, {
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' }
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
var bundle = data.fhir_bundle || data.bundle || (data.resourceType === 'Bundle' ? data : null);
|
||||
document.getElementById('patientDocJsonView').textContent = JSON.stringify(data, null, 2);
|
||||
if (bundle) {
|
||||
document.getElementById('patientDocFhirBadge').className = 'badge bg-success';
|
||||
document.getElementById('patientDocFhirBadge').textContent = 'HAS FHIR';
|
||||
var html = '<div class="card mb-3 border-primary-subtle"><div class="card-header bg-primary text-white py-1 font-monospace fw-bold">' + (data.source?.document_title || docTitle || 'Health Document') + '</div>';
|
||||
html += '<div class="card-body py-2 small">';
|
||||
html += '<p class="mb-1"><strong>Patient:</strong> ' + (data.source?.patient?.name || '-') + ' (ABHA: ' + (data.source?.patient?.abha_id || '-') + ')</p>';
|
||||
html += '<p class="mb-1"><strong>Content Type:</strong> <code>' + (data.source?.content_type || 'application/pdf') + '</code></p>';
|
||||
html += '<p class="mb-1"><strong>Facility HFR:</strong> ' + (data.source?.hfr_id || '-') + ' (' + (data.source?.organization?.name || '-') + ')</p>';
|
||||
html += '<p class="mb-0"><strong>Care Context:</strong> DOC-' + docId + ' (' + (data.source?.visit_date || '') + ')</p>';
|
||||
html += '</div></div>';
|
||||
document.getElementById('patientDocFormView').innerHTML = html;
|
||||
} else {
|
||||
document.getElementById('patientDocFhirBadge').className = 'badge bg-warning';
|
||||
document.getElementById('patientDocFhirBadge').textContent = 'NOT READY';
|
||||
document.getElementById('patientDocFormView').innerHTML = '<div class="alert alert-warning">Unable to build FHIR bundle for this document. Check file upload details.</div>';
|
||||
}
|
||||
})
|
||||
.catch(function(err) {
|
||||
document.getElementById('patientDocFhirBadge').className = 'badge bg-danger';
|
||||
document.getElementById('patientDocFhirBadge').textContent = 'ERROR';
|
||||
document.getElementById('patientDocFormView').innerHTML = '<div class="alert alert-danger">Failed to load FHIR preview: ' + err.message + '</div>';
|
||||
});
|
||||
}
|
||||
|
||||
function generateSingleDocumentFhir(docId) {
|
||||
var btn = document.getElementById('btn-fhir-single-' + docId);
|
||||
if (btn) { btn.disabled = true; btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Pushing...'; }
|
||||
|
||||
$.post('<?= base_url('patient/generate_fhir_for_file') ?>/' + docId, function(res) {
|
||||
if (res && res.ok === 1) {
|
||||
alert(res.message || 'FHIR HealthDocumentRecord bundle generated and enqueued for ABDM sync!');
|
||||
location.reload();
|
||||
} else {
|
||||
alert((res && res.error_text) ? res.error_text : 'Unable to generate FHIR for document.');
|
||||
if (btn) { btn.disabled = false; btn.innerHTML = '<i class="bi bi-lightning-charge me-1"></i>Push ABDM'; }
|
||||
}
|
||||
}, 'json').fail(function() {
|
||||
alert('Network request failed while pushing to ABDM.');
|
||||
if (btn) { btn.disabled = false; btn.innerHTML = '<i class="bi bi-lightning-charge me-1"></i>Push ABDM'; }
|
||||
});
|
||||
}
|
||||
|
||||
function generateAllFhirDocuments(patientId) {
|
||||
var btn = document.getElementById('btnGenerateAllFhirDocs');
|
||||
if (btn) { btn.disabled = true; btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Bundling FHIR...'; }
|
||||
|
||||
$.post('<?= base_url('patient/generate_fhir_bundle_for_all_files') ?>/' + patientId, function(res) {
|
||||
if (res && res.ok === 1) {
|
||||
alert(res.message || 'Scanned documents bundled into HealthDocumentRecord FHIR tasks!');
|
||||
location.reload();
|
||||
} else {
|
||||
alert((res && res.error_text) ? res.error_text : 'Failed to bundle documents.');
|
||||
if (btn) { btn.disabled = false; btn.innerHTML = '<i class="bi bi-diagram-3-fill me-1"></i>Generate ABDM FHIR Bundle (All)'; }
|
||||
}
|
||||
}, 'json').fail(function() {
|
||||
alert('Network request failed.');
|
||||
if (btn) { btn.disabled = false; btn.innerHTML = '<i class="bi bi-diagram-3-fill me-1"></i>Generate ABDM FHIR Bundle (All)'; }
|
||||
});
|
||||
}
|
||||
|
||||
function previewWellnessFhir(patientId, opdId) {
|
||||
var modalEl = document.getElementById('patientDocFhirModal');
|
||||
var modal = bootstrap.Modal.getOrCreateInstance(modalEl);
|
||||
document.getElementById('patientDocFhirTitle').innerHTML = '<i class="bi bi-activity me-2"></i>WellnessRecord FHIR Bundle Preview';
|
||||
document.getElementById('patientDocFormView').innerHTML = '<div class="text-center text-muted py-4"><div class="spinner-border spinner-border-sm text-primary me-2"></div>Building ABDM WellnessRecord FHIR bundle...</div>';
|
||||
document.getElementById('patientDocJsonView').textContent = 'Loading...';
|
||||
modal.show();
|
||||
|
||||
var url = '<?= base_url('DoctorDocument/wellness_record_fhir_preview') ?>/' + patientId + (opdId ? ('/' + opdId) : '');
|
||||
fetch(url, {
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' }
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
var bundle = data.fhir_bundle || data.bundle || (data.resourceType === 'Bundle' ? data : null);
|
||||
document.getElementById('patientDocJsonView').textContent = JSON.stringify(data, null, 2);
|
||||
if (bundle) {
|
||||
document.getElementById('patientDocFhirBadge').className = 'badge bg-success';
|
||||
document.getElementById('patientDocFhirBadge').textContent = 'HAS FHIR';
|
||||
document.getElementById('patientDocProfileBadge').textContent = 'WellnessRecord';
|
||||
var html = '<div class="card mb-3 border-success-subtle"><div class="card-header bg-success text-white py-1 font-monospace fw-bold"><i class="bi bi-activity me-1"></i>Wellness & Vitals Assessment</div>';
|
||||
html += '<div class="card-body py-2 small">';
|
||||
html += '<p class="mb-1"><strong>Patient:</strong> ' + (data.source?.patient?.name || '-') + ' (ABHA: ' + (data.source?.patient?.abha_id || '-') + ')</p>';
|
||||
html += '<p class="mb-1"><strong>Doctor:</strong> ' + (data.source?.doctor_name || '-') + '</p>';
|
||||
html += '<p class="mb-1"><strong>Vitals Count:</strong> ' + (data.source?.vitals?.length || 0) + ' recorded observations</p>';
|
||||
if (data.source?.women_wellness && Object.keys(data.source.women_wellness).length > 0) {
|
||||
html += '<p class="mb-1"><strong>Women Wellness:</strong> ' + JSON.stringify(data.source.women_wellness) + '</p>';
|
||||
}
|
||||
html += '<p class="mb-0"><strong>Care Context:</strong> ' + (data.care_context_reference || 'WELLNESS-' + patientId) + '</p>';
|
||||
html += '</div></div>';
|
||||
document.getElementById('patientDocFormView').innerHTML = html;
|
||||
} else {
|
||||
document.getElementById('patientDocFhirBadge').className = 'badge bg-warning';
|
||||
document.getElementById('patientDocFhirBadge').textContent = 'NOT READY';
|
||||
document.getElementById('patientDocFormView').innerHTML = '<div class="alert alert-warning">No vitals or wellness observations found to build WellnessRecord FHIR bundle.</div>';
|
||||
}
|
||||
})
|
||||
.catch(function(err) {
|
||||
document.getElementById('patientDocFhirBadge').className = 'badge bg-danger';
|
||||
document.getElementById('patientDocFhirBadge').textContent = 'ERROR';
|
||||
document.getElementById('patientDocFormView').innerHTML = '<div class="alert alert-danger">Failed to load FHIR preview: ' + err.message + '</div>';
|
||||
});
|
||||
}
|
||||
|
||||
function generateWellnessFhir(patientId, opdId) {
|
||||
var url = '<?= base_url('patient/generate_wellness_fhir') ?>/' + patientId + (opdId ? ('/' + opdId) : '');
|
||||
$.post(url, function(res) {
|
||||
if (res && res.ok === 1) {
|
||||
alert(res.message || 'WellnessRecord FHIR bundle enqueued for ABDM sync!');
|
||||
location.reload();
|
||||
} else {
|
||||
alert((res && res.error_text) ? res.error_text : 'Unable to generate WellnessRecord FHIR.');
|
||||
}
|
||||
}, 'json').fail(function() {
|
||||
alert('Network request failed.');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,100 +1,355 @@
|
|||
<div class="pagetitle">
|
||||
<h1>Doctor Documents Workspace</h1>
|
||||
<nav>
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="<?= base_url('dashboard') ?>">Home</a></li>
|
||||
<li class="breadcrumb-item active">Doctor Documents</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||
<div>
|
||||
<h1><i class="bi bi-folder-check text-primary me-2"></i>MRD & IPD Medical Record Documents Workspace</h1>
|
||||
<nav>
|
||||
<ol class="breadcrumb mb-0">
|
||||
<li class="breadcrumb-item"><a href="<?= base_url('dashboard') ?>">Home</a></li>
|
||||
<li class="breadcrumb-item active">MRD & IPD Document Scanner</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
<div>
|
||||
<span class="badge bg-success px-3 py-2 fs-6"><i class="bi bi-shield-check me-1"></i>NABH Guidelines Compliant</span>
|
||||
<span class="badge bg-primary px-3 py-2 fs-6 ms-1"><i class="bi bi-cloud-arrow-up me-1"></i>ABDM FHIR R4 Ready</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="section">
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Template Master</h5>
|
||||
<p class="text-muted mb-3">Create and edit certificate templates with custom fields.</p>
|
||||
<button type="button" class="btn btn-primary" onclick="load_form('<?= base_url('Doc_Admin/doc_list') ?>')">
|
||||
Open Template List
|
||||
<section class="section mt-3">
|
||||
<!-- MAIN NABH IPD MEDICAL RECORD SCANNER & ABDM HEALTH DOCUMENT PORTAL -->
|
||||
<div class="card shadow border-0 mb-4">
|
||||
<div class="card-header bg-gradient-primary text-white d-flex justify-content-between align-items-center py-3" style="background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);">
|
||||
<div>
|
||||
<h5 class="m-0 text-white fw-bold"><i class="bi bi-scanner me-2"></i>NABH IPD Medical Record Scanner & ABDM Health Document Upload</h5>
|
||||
<small class="text-white-50">Type IPD No. to view required paper scan checklist as per NABH guidelines</small>
|
||||
</div>
|
||||
<span class="badge bg-warning text-dark"><i class="bi bi-star-fill me-1"></i>NABH IPD Checklist</span>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<!-- IPD Search Box -->
|
||||
<div class="row g-3 align-items-end mb-4">
|
||||
<div class="col-md-7 col-lg-8">
|
||||
<label class="form-label fw-bold" for="nabh_ipd_search_input">
|
||||
<i class="bi bi-search text-primary me-1"></i>Enter IPD No. / Admission No. / UHID / Patient Name
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-light"><i class="bi bi-hospital"></i></span>
|
||||
<input type="text" class="form-control form-control-lg" id="nabh_ipd_search_input" placeholder="e.g. IPD-2026-001, 17, 5, DEVENDER..." onkeypress="if(event.key==='Enter'){ searchIpdPatientForNabh(event); return false; }">
|
||||
<button type="button" class="btn btn-lg btn-primary px-4" id="btn_search_ipd_nabh" onclick="searchIpdPatientForNabh(event)">
|
||||
<i class="bi bi-search me-1"></i> Search IPD Admission
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-5 col-lg-4 text-end">
|
||||
<small class="text-muted d-block">Need help locating IPD admission?</small>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary mt-1" onclick="load_form('<?= base_url('DoctorDocument/workspace') ?>')">
|
||||
<i class="bi bi-arrow-clockwise me-1"></i> Refresh Workspace
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Patient Document Issue</h5>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="doc_workspace_patient_id">Patient ID / UHID</label>
|
||||
<input type="text" class="form-control" id="doc_workspace_patient_id" placeholder="Enter UHID / Patient ID / last digits">
|
||||
</div>
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
<button type="button" class="btn btn-success" onclick="openPatientDoc()">Open Patient Documents</button>
|
||||
<button type="button" class="btn btn-outline-success" onclick="openPatientImmunization()">Open Immunization</button>
|
||||
<button type="button" class="btn btn-outline-secondary" onclick="load_form('<?= base_url('doctor_work/patient_search') ?>','Doctor Patient Search')">Search Patient with History</button>
|
||||
<button type="button" class="btn btn-outline-primary" onclick="load_form('<?= base_url('Report/document_list') ?>', 'Document Issue Report')">Document Issue Report</button>
|
||||
|
||||
<!-- Patient Info Banner (Hidden until searched) -->
|
||||
<div id="nabh_ipd_patient_card" class="card bg-light border border-info mb-4" style="display: none;">
|
||||
<div class="card-body p-3">
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||
<div>
|
||||
<h5 class="fw-bold mb-1 text-primary" id="nabh_ipd_patient_name">DEVENDER SINGH</h5>
|
||||
<div class="d-flex flex-wrap gap-3 text-dark small">
|
||||
<span><strong>IPD No:</strong> <span class="badge bg-primary fs-6" id="nabh_ipd_no">IPD-17</span></span>
|
||||
<span><strong>UHID:</strong> <span class="badge bg-secondary fs-6" id="nabh_ipd_uhid">11</span></span>
|
||||
<span><strong>Gender/Age:</strong> <span id="nabh_ipd_gender_age">Male / 47</span></span>
|
||||
<span><strong>Admission Date:</strong> <span id="nabh_ipd_admission_date">30-07-2026</span></span>
|
||||
<span><strong>Attending Doctor:</strong> <span id="nabh_ipd_doctor">Dr. Nidhi Pandey</span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="badge bg-info text-dark p-2 me-1" id="nabh_ipd_abha_badge"><i class="bi bi-qr-code me-1"></i>ABHA Address</span>
|
||||
<small class="d-block text-muted text-end fw-bold" id="nabh_ipd_abha_address">singhdevender0328@sbx</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- NABH Guideline Required Documents Grid (Hidden until patient selected) -->
|
||||
<div id="nabh_ipd_checklist_section" style="display: none;">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 class="fw-bold text-dark m-0"><i class="bi bi-check2-square text-success me-2"></i>NABH Guideline Mandatory IPD Paper Documents Checklist</h6>
|
||||
<small class="text-muted">Upload scanned paper records (JPG, PNG, PDF) & push as FHIR HealthDocumentRecord to ABDM</small>
|
||||
</div>
|
||||
|
||||
<div class="row g-3" id="nabh_doc_categories_container">
|
||||
<!-- Populated dynamically via JavaScript -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Initial Placeholder -->
|
||||
<div id="nabh_ipd_placeholder" class="text-center py-5 border rounded bg-light">
|
||||
<i class="bi bi-folder-symlink display-4 text-muted"></i>
|
||||
<h5 class="mt-3 text-muted">Search an IPD Admission to View NABH Document Upload List</h5>
|
||||
<p class="text-muted small">Enter an IPD No. (e.g. 17 or 5) above to load the NABH mandatory scanned document checklist.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
function openPatientDoc() {
|
||||
var patientKey = (document.getElementById('doc_workspace_patient_id').value || '').trim();
|
||||
if (!patientKey) {
|
||||
alert('Enter patient id');
|
||||
return;
|
||||
var _activeIpdRecord = null;
|
||||
|
||||
var _nabhCategories = [
|
||||
{ code: 'NABH-IPD-01', title: 'Admission Request & Initial Assessment Form', icon: 'bi-file-earmark-text', desc: 'Patient admission order, initial clinical assessment, emergency notes & vital baseline.' },
|
||||
{ code: 'NABH-IPD-02', title: 'Signed Informed Consent Forms', icon: 'bi-pen-fill', desc: 'Signed consent forms for general admission, surgery, anesthesia, high risk procedures & blood transfusion.' },
|
||||
{ code: 'NABH-IPD-03', title: 'Doctor Clinical Notes & Daily Orders', icon: 'bi-journal-medical', desc: 'Daily clinical progress notes, doctor rounds, medication orders & treatment plans.' },
|
||||
{ code: 'NABH-IPD-04', title: 'Nursing Flowsheet & Vitals Chart', icon: 'bi-activity', desc: '24-hour nursing assessment, vital signs monitoring chart, fluid intake/output & drug administration record.' },
|
||||
{ code: 'NABH-IPD-05', title: 'OT Surgery Notes & Anesthesia Record', icon: 'bi-scissors', desc: 'Pre-operative evaluation, intra-operative surgical notes, anesthesia record & post-op recovery note.' },
|
||||
{ code: 'NABH-IPD-06', title: 'Laboratory & Radiology Diagnostic Reports', icon: 'bi-file-earmark-pdf', desc: 'Scanned paper lab investigation reports, ECG charts, X-ray & CT/MRI imaging reports.' },
|
||||
{ code: 'NABH-IPD-07', title: 'Discharge Summary & Advice Note', icon: 'bi-file-earmark-check', desc: 'NABH discharge summary, discharge instructions, prescription & follow-up advice.' },
|
||||
{ code: 'NABH-IPD-08', title: 'Billing Summary & TPA/Insurance Claim Form', icon: 'bi-credit-card-2-front', desc: 'IPD final bill summary, government scheme approval, insurance claim forms & payment receipts.' },
|
||||
{ code: 'NABH-IPD-09', title: 'Other Scanned Paper Medical Records', icon: 'bi-folder-plus', desc: 'Any other paper medical records, external referral slips, or patient consent attachments.' }
|
||||
];
|
||||
|
||||
function searchIpdPatientForNabh(ev) {
|
||||
if (ev && ev.preventDefault) ev.preventDefault();
|
||||
if (ev && ev.stopPropagation) ev.stopPropagation();
|
||||
|
||||
var query = (document.getElementById('nabh_ipd_search_input').value || '').trim();
|
||||
if (!query) {
|
||||
alert('Please enter an IPD No., UHID, or Patient Name.');
|
||||
return false;
|
||||
}
|
||||
|
||||
var resolveUrl = '<?= base_url('Document_Patient/open_by_key') ?>?patient_key=' + encodeURIComponent(patientKey);
|
||||
fetch(resolveUrl, {
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
})
|
||||
.then(function(response) {
|
||||
return response.json();
|
||||
})
|
||||
var btn = document.getElementById('btn_search_ipd_nabh');
|
||||
var origHtml = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span> Searching...';
|
||||
|
||||
var searchUrl = '<?= base_url('DoctorDocument/search_ipd_patient') ?>?ipd_key=' + encodeURIComponent(query);
|
||||
fetch(searchUrl, { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data && Number(data.status) === 1 && Number(data.patient_id) > 0) {
|
||||
load_form('<?= base_url('Document_Patient/p_doc_record') ?>/' + Number(data.patient_id));
|
||||
return;
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = origHtml;
|
||||
|
||||
if (data.status === 'success' && Array.isArray(data.records) && data.records.length > 0) {
|
||||
_activeIpdRecord = data.records[0];
|
||||
renderActiveIpdRecord(_activeIpdRecord);
|
||||
} else {
|
||||
alert((data && data.message) ? data.message : 'No IPD admission found matching "' + query + '"');
|
||||
}
|
||||
alert((data && data.message) ? data.message : 'Patient not found');
|
||||
})
|
||||
.catch(function() {
|
||||
alert('Unable to resolve patient. Try again.');
|
||||
.catch(function(e) {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = origHtml;
|
||||
alert('Unable to search IPD patient: ' + e.message);
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function openPatientImmunization() {
|
||||
var patientKey = (document.getElementById('doc_workspace_patient_id').value || '').trim();
|
||||
if (!patientKey) {
|
||||
alert('Enter patient id');
|
||||
return;
|
||||
function renderActiveIpdRecord(rec) {
|
||||
document.getElementById('nabh_ipd_placeholder').style.display = 'none';
|
||||
document.getElementById('nabh_ipd_patient_card').style.display = 'block';
|
||||
document.getElementById('nabh_ipd_checklist_section').style.display = 'block';
|
||||
|
||||
document.getElementById('nabh_ipd_patient_name').textContent = rec.patient_name || 'Patient';
|
||||
document.getElementById('nabh_ipd_no').textContent = rec.ipd_no || ('IPD-' + rec.ipd_id);
|
||||
document.getElementById('nabh_ipd_uhid').textContent = rec.uhid || rec.patient_id;
|
||||
document.getElementById('nabh_ipd_gender_age').textContent = (rec.gender || 'N/A') + ' / ' + (rec.age || 'N/A');
|
||||
document.getElementById('nabh_ipd_admission_date').textContent = rec.admission_date || 'N/A';
|
||||
document.getElementById('nabh_ipd_doctor').textContent = rec.doctor_name || 'Attending Doctor';
|
||||
document.getElementById('nabh_ipd_abha_address').textContent = rec.abha_address || rec.abha_id || 'Not linked';
|
||||
|
||||
var container = document.getElementById('nabh_doc_categories_container');
|
||||
container.innerHTML = '';
|
||||
|
||||
_nabhCategories.forEach(function(cat) {
|
||||
var existing = (rec.scans || {})[cat.code] || null;
|
||||
var col = document.createElement('div');
|
||||
col.className = 'col-md-6 col-lg-4';
|
||||
|
||||
var statusBadge = '<span class="badge bg-secondary mb-2" id="badge_status_' + cat.code + '"><i class="bi bi-clock me-1"></i>Pending Scan</span>';
|
||||
if (existing) {
|
||||
if (existing.abdm_status === 'pushed') {
|
||||
statusBadge = '<span class="badge bg-success mb-2" id="badge_status_' + cat.code + '"><i class="bi bi-check-circle-fill me-1"></i>Pushed to ABDM (201)</span>';
|
||||
} else {
|
||||
statusBadge = '<span class="badge bg-info text-dark mb-2" id="badge_status_' + cat.code + '"><i class="bi bi-file-earmark-check me-1"></i>Scanned & Saved</span>';
|
||||
}
|
||||
}
|
||||
|
||||
var fileInfo = existing ? '<div class="small text-truncate text-muted mt-1" id="file_info_' + cat.code + '"><i class="bi bi-paperclip me-1"></i><a href="' + existing.file_url + '" target="_blank" class="fw-bold text-decoration-none">' + hesc(existing.file_name) + '</a></div>' : '<div class="small text-muted mt-1" id="file_info_' + cat.code + '">No scan uploaded yet.</div>';
|
||||
|
||||
var pushBtnDisabled = existing ? '' : 'disabled';
|
||||
var pushBtnClass = (existing && existing.abdm_status === 'pushed') ? 'btn-outline-success' : 'btn-primary';
|
||||
var pushBtnText = (existing && existing.abdm_status === 'pushed') ? '<i class="bi bi-check2-circle"></i> Pushed to ABDM' : '<i class="bi bi-cloud-arrow-up"></i> Push to ABDM';
|
||||
|
||||
col.innerHTML = `
|
||||
<div class="card h-100 shadow-sm border border-light-subtle">
|
||||
<div class="card-body p-3 d-flex flex-column justify-content-between">
|
||||
<div>
|
||||
<div class="d-flex justify-content-between align-items-start gap-1">
|
||||
<h6 class="fw-bold text-dark mb-1">
|
||||
<i class="bi ${cat.icon} text-primary me-2"></i>${hesc(cat.title)}
|
||||
</h6>
|
||||
</div>
|
||||
${statusBadge}
|
||||
<p class="text-muted small mb-2" style="font-size:0.8rem; line-height:1.2;">${hesc(cat.desc)}</p>
|
||||
${fileInfo}
|
||||
</div>
|
||||
<div class="mt-3 pt-2 border-top">
|
||||
<div class="mb-2">
|
||||
<input type="file" class="form-control form-control-sm" id="file_input_${cat.code}" accept=".jpg,.jpeg,.png,.webp,.pdf">
|
||||
</div>
|
||||
<div class="d-flex gap-1">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary flex-grow-1" onclick="uploadNabhScan(event, '${cat.code}', '${hesc(cat.title)}')">
|
||||
<i class="bi bi-upload"></i> Upload
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm ${pushBtnClass} flex-grow-1" id="btn_push_${cat.code}" ${pushBtnDisabled} onclick="pushNabhToAbdm(event, '${cat.code}', ${existing ? existing.patient_doc_id : 0})">
|
||||
${pushBtnText}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(col);
|
||||
});
|
||||
}
|
||||
|
||||
function uploadNabhScan(ev, catCode, catTitle) {
|
||||
if (ev && ev.preventDefault) ev.preventDefault();
|
||||
if (ev && ev.stopPropagation) ev.stopPropagation();
|
||||
|
||||
if (!_activeIpdRecord) {
|
||||
alert('Please search and select an IPD admission first.');
|
||||
return false;
|
||||
}
|
||||
|
||||
var resolveUrl = '<?= base_url('Document_Patient/open_by_key') ?>?patient_key=' + encodeURIComponent(patientKey);
|
||||
fetch(resolveUrl, {
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
var fileInput = document.getElementById('file_input_' + catCode);
|
||||
if (!fileInput || !fileInput.files || fileInput.files.length === 0) {
|
||||
alert('Please choose a scanned paper file (JPG, PNG, PDF) to upload for ' + catTitle);
|
||||
return false;
|
||||
}
|
||||
|
||||
var btn = ev ? (ev.currentTarget || ev.target) : null;
|
||||
var origHtml = btn ? btn.innerHTML : '';
|
||||
if (btn) {
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span>';
|
||||
}
|
||||
|
||||
var formData = new FormData();
|
||||
formData.append('ipd_id', _activeIpdRecord.ipd_id);
|
||||
formData.append('patient_id', _activeIpdRecord.patient_id);
|
||||
formData.append('doctor_id', _activeIpdRecord.doctor_id || 1);
|
||||
formData.append('nabh_category', catCode);
|
||||
formData.append('category_title', catTitle);
|
||||
formData.append('scanned_file', fileInput.files[0]);
|
||||
|
||||
var uploadUrl = '<?= base_url('DoctorDocument/upload_ipd_nabh_document') ?>';
|
||||
fetch(uploadUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
||||
body: formData
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(res) {
|
||||
if (btn) {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = origHtml;
|
||||
}
|
||||
|
||||
if (res.status === 'success') {
|
||||
alert('✅ ' + res.message);
|
||||
var badge = document.getElementById('badge_status_' + catCode);
|
||||
if (badge) {
|
||||
badge.className = 'badge bg-info text-dark mb-2';
|
||||
badge.innerHTML = '<i class="bi bi-file-earmark-check me-1"></i>Scanned & Saved';
|
||||
}
|
||||
var info = document.getElementById('file_info_' + catCode);
|
||||
if (info) {
|
||||
info.innerHTML = '<i class="bi bi-paperclip me-1"></i><a href="' + res.file_url + '" target="_blank" class="fw-bold text-decoration-none">' + hesc(res.file_name) + '</a>';
|
||||
}
|
||||
var pushBtn = document.getElementById('btn_push_' + catCode);
|
||||
if (pushBtn) {
|
||||
pushBtn.disabled = false;
|
||||
pushBtn.setAttribute('onclick', "pushNabhToAbdm(event, '" + catCode + "', " + res.patient_doc_id + ")");
|
||||
}
|
||||
} else {
|
||||
alert('❌ Upload failed: ' + (res.message || 'Unknown error'));
|
||||
}
|
||||
})
|
||||
.then(function(response) {
|
||||
return response.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
if (data && Number(data.status) === 1 && Number(data.patient_id) > 0) {
|
||||
load_form('<?= base_url('Immunization') ?>?patient_id=' + Number(data.patient_id), 'Immunization Record');
|
||||
return;
|
||||
.catch(function(e) {
|
||||
if (btn) {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = origHtml;
|
||||
}
|
||||
alert('❌ Error uploading scan: ' + e.message);
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function pushNabhToAbdm(ev, catCode, patientDocId) {
|
||||
if (ev && ev.preventDefault) ev.preventDefault();
|
||||
if (ev && ev.stopPropagation) ev.stopPropagation();
|
||||
|
||||
if (patientDocId <= 0) {
|
||||
alert('Upload a scanned file first before pushing to ABDM.');
|
||||
return false;
|
||||
}
|
||||
|
||||
var btn = document.getElementById('btn_push_' + catCode);
|
||||
var origHtml = btn ? btn.innerHTML : '';
|
||||
if (btn) {
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span> Pushing...';
|
||||
}
|
||||
|
||||
var formData = new URLSearchParams();
|
||||
formData.append('patient_doc_id', patientDocId);
|
||||
formData.append('ipd_id', _activeIpdRecord ? _activeIpdRecord.ipd_id : 0);
|
||||
formData.append('nabh_category', catCode);
|
||||
|
||||
fetch('<?= base_url('DoctorDocument/push_ipd_nabh_to_abdm') ?>', {
|
||||
method: 'POST',
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: formData.toString()
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(res) {
|
||||
if (btn) {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = origHtml;
|
||||
}
|
||||
|
||||
if (res.ok === 1 || res.status === 'queued') {
|
||||
alert('🚀 Successfully pushed HealthDocumentRecord to ABDM Gateway Bridge!\nCare Context: ' + (res.care_context_reference || 'DOC-' + patientDocId));
|
||||
var badge = document.getElementById('badge_status_' + catCode);
|
||||
if (badge) {
|
||||
badge.className = 'badge bg-success mb-2';
|
||||
badge.innerHTML = '<i class="bi bi-check-circle-fill me-1"></i>Pushed to ABDM (201)';
|
||||
}
|
||||
alert((data && data.message) ? data.message : 'Patient not found');
|
||||
})
|
||||
.catch(function() {
|
||||
alert('Unable to resolve patient. Try again.');
|
||||
});
|
||||
if (btn) {
|
||||
btn.className = 'btn btn-sm btn-outline-success flex-grow-1';
|
||||
btn.innerHTML = '<i class="bi bi-check2-circle"></i> Pushed to ABDM';
|
||||
}
|
||||
} else {
|
||||
alert('❌ ABDM Push Failed: ' + (res.error || res.error_text || 'Bridge rejected request'));
|
||||
}
|
||||
})
|
||||
.catch(function(e) {
|
||||
if (btn) {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = origHtml;
|
||||
}
|
||||
alert('❌ Error pushing to ABDM: ' + e.message);
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function hesc(str) {
|
||||
if (!str) return '';
|
||||
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -487,6 +487,12 @@ if (! $canChargesSettings && $user && method_exists($user, 'inGroup')) {
|
|||
<span>Collection Report</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link collapsed" href="javascript:load_form('<?= base_url('Report/old_payment_received_report') ?>','Old Payment Received Report')">
|
||||
<i class="bi bi-clock-history"></i>
|
||||
<span>Old Payment Received</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link collapsed" href="javascript:load_form('<?= base_url('Report/report_opd_total') ?>','OPD Total Report')">
|
||||
<i class="bi bi-bar-chart"></i>
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ $can = static fn (string $panel): bool => (bool) ($panels[$panel] ?? false);
|
|||
</div>
|
||||
<ul class="report-links">
|
||||
<li><a href="javascript:load_form('<?= base_url('Report/collection_report') ?>','Collection Report');"><i class="bi bi-arrow-right-short"></i> Total Payment (Cash and Bank)</a></li>
|
||||
<li><a href="javascript:load_form('<?= base_url('Report/old_payment_received_report') ?>','Old Payment Received Report');"><i class="bi bi-arrow-right-short"></i> Old Payment Received Report (Late Collections)</a></li>
|
||||
<li><a href="javascript:load_form('<?= base_url('Report/report_opd_total') ?>','OPD Total Report');"><i class="bi bi-arrow-right-short"></i> OPD Total Report</a></li>
|
||||
<li><a href="javascript:load_form('<?= base_url('Report/report_opd_patient_list') ?>','OPD Patient List');"><i class="bi bi-arrow-right-short"></i> OPD Patient List</a></li>
|
||||
</ul>
|
||||
|
|
|
|||
89
app/Views/report/old_payment_received_report.php
Normal file
89
app/Views/report/old_payment_received_report.php
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
<div class="card">
|
||||
<div class="card-header d-flex align-items-center justify-content-between flex-wrap gap-2">
|
||||
<h3 class="card-title mb-0"><i class="bi bi-clock-history text-primary me-2"></i>Old Payment Received Report</h3>
|
||||
<span class="badge bg-info text-dark"><i class="bi bi-calendar-range me-1"></i>Invoice Date < Payment Date</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted small mb-3">
|
||||
This report lists payments received during the selected date range where the original invoice was generated on an <strong>earlier date</strong> (Invoice Date < Payment Date).
|
||||
</p>
|
||||
|
||||
<form class="row g-2 align-items-end mb-3" onsubmit="event.preventDefault(); loadOldPaymentData(0);">
|
||||
<div class="col-md-5">
|
||||
<label class="form-label fw-bold">Payment Collection Date Range</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="old_pay_min" placeholder="Start Date" value="<?= date('Y-m-d 00:00:00') ?>">
|
||||
<span class="input-group-text">to</span>
|
||||
<input type="text" class="form-control" id="old_pay_max" placeholder="End Date" value="<?= date('Y-m-d 23:59:00') ?>">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-7 d-flex gap-2 flex-wrap">
|
||||
<button type="button" class="btn btn-primary" onclick="loadOldPaymentData(0)">
|
||||
<i class="bi bi-search me-1"></i> Show Details
|
||||
</button>
|
||||
<button type="button" class="btn btn-success" onclick="loadOldPaymentData(1)">
|
||||
<i class="bi bi-file-earmark-excel me-1"></i> Export Excel
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger" onclick="loadOldPaymentData(2)">
|
||||
<i class="bi bi-file-earmark-pdf me-1"></i> Export PDF
|
||||
</button>
|
||||
<button type="button" class="btn btn-light" onclick="load_form('<?= base_url('Report/old_payment_received_report') ?>','Old Payment Received Report')">
|
||||
<i class="bi bi-arrow-clockwise me-1"></i> Reset
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div id="old_payment_results_container" class="mt-3">
|
||||
<div class="text-center py-5 border rounded bg-light">
|
||||
<i class="bi bi-search display-6 text-muted"></i>
|
||||
<p class="text-muted mt-2">Click <strong>Show Details</strong> to load old payment collections for the selected date range.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function buildDateRangeStr() {
|
||||
var minVal = (document.getElementById('old_pay_min').value || '').trim();
|
||||
var maxVal = (document.getElementById('old_pay_max').value || '').trim();
|
||||
if (!minVal) minVal = '<?= date('Y-m-d 00:00:00') ?>';
|
||||
if (!maxVal) maxVal = '<?= date('Y-m-d 23:59:00') ?>';
|
||||
|
||||
return encodeURIComponent(minVal) + 'S' + encodeURIComponent(maxVal);
|
||||
}
|
||||
|
||||
function loadOldPaymentData(outputType) {
|
||||
var dateRange = buildDateRangeStr();
|
||||
var url = '<?= base_url('Report/old_payment_received_report_data') ?>/' + dateRange + '/' + (outputType || 0);
|
||||
|
||||
if (outputType === 1 || outputType === 2) {
|
||||
window.open(url, '_blank');
|
||||
return;
|
||||
}
|
||||
|
||||
var container = document.getElementById('old_payment_results_container');
|
||||
container.innerHTML = '<div class="text-center py-5"><span class="spinner-border text-primary"></span><p class="mt-2 text-muted">Loading Old Payment Received Data...</p></div>';
|
||||
|
||||
fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
||||
.then(function(r) { return r.text(); })
|
||||
.then(function(html) {
|
||||
container.innerHTML = html;
|
||||
if (window.jQuery && $.fn && $.fn.DataTable) {
|
||||
var $t = $('#oldPaymentTable');
|
||||
if ($t.length > 0 && $t.find('tbody tr').length > 0) {
|
||||
if ($.fn.DataTable.isDataTable($t)) {
|
||||
$t.DataTable().destroy();
|
||||
}
|
||||
$t.DataTable({ pageLength: 25, order: [[0, 'desc']] });
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(function(e) {
|
||||
container.innerHTML = '<div class="alert alert-danger">Unable to load report: ' + e.message + '</div>';
|
||||
});
|
||||
}
|
||||
|
||||
(function() {
|
||||
loadOldPaymentData(0);
|
||||
})();
|
||||
</script>
|
||||
64
app/Views/report/old_payment_received_report_table.php
Normal file
64
app/Views/report/old_payment_received_report_table.php
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
<div class="alert alert-info d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
<i class="bi bi-info-circle-fill me-2"></i>
|
||||
Payment Date Range: <strong><?= esc($min_range ?? '') ?></strong> to <strong><?= esc($max_range ?? '') ?></strong>
|
||||
</div>
|
||||
<div>
|
||||
Total Records: <span class="badge bg-primary fs-6"><?= count($rows ?? []) ?></span> |
|
||||
Total Amount Collected: <span class="badge bg-success fs-6">₹<?= number_format((float) ($total_amount ?? 0), 2, '.', ',') ?></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (empty($rows)): ?>
|
||||
<div class="text-center py-4 border rounded bg-light text-muted">
|
||||
<i class="bi bi-check-circle-fill text-success fs-4 d-block mb-2"></i>
|
||||
<h5>No Old Payment Receipts Found</h5>
|
||||
<p class="mb-0 small">No transactions found where Invoice Date < Payment Date in the selected date range.</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover align-middle border" id="oldPaymentTable">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Payment Date</th>
|
||||
<th>Invoice Date</th>
|
||||
<th>Delay (Days)</th>
|
||||
<th>Invoice Code</th>
|
||||
<th>Patient Name</th>
|
||||
<th>Patient Code</th>
|
||||
<th class="text-end">Invoice Net Amt</th>
|
||||
<th class="text-end">Amount Received</th>
|
||||
<th>Pay Mode</th>
|
||||
<th>Collected By</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php $i = 1; foreach ($rows as $row): ?>
|
||||
<?php
|
||||
$payDate = ! empty($row['payment_date']) ? date('d-m-Y H:i', strtotime((string) $row['payment_date'])) : '-';
|
||||
$invDate = ! empty($row['inv_date']) ? date('d-m-Y', strtotime((string) $row['inv_date'])) : '-';
|
||||
$delay = (int) ($row['delay_days'] ?? 0);
|
||||
?>
|
||||
<tr>
|
||||
<td><?= $i++ ?></td>
|
||||
<td><span class="badge bg-secondary"><?= esc($payDate) ?></span></td>
|
||||
<td><span class="badge bg-warning text-dark"><?= esc($invDate) ?></span></td>
|
||||
<td>
|
||||
<span class="badge bg-danger-subtle text-danger font-monospace">
|
||||
<i class="bi bi-clock-history me-1"></i><?= $delay ?> day<?= $delay > 1 ? 's' : '' ?> ago
|
||||
</span>
|
||||
</td>
|
||||
<td class="fw-bold"><?= esc((string) ($row['invoice_code'] ?? '-')) ?></td>
|
||||
<td><?= esc((string) ($row['patient_name'] ?? '-')) ?></td>
|
||||
<td><?= esc((string) ($row['patient_code'] ?? '-')) ?></td>
|
||||
<td class="text-end fw-bold"><?= number_format((float) ($row['net_amount'] ?? 0), 2, '.', ',') ?></td>
|
||||
<td class="text-end fw-bold text-success">₹<?= number_format((float) ($row['paid_amount'] ?? 0), 2, '.', ',') ?></td>
|
||||
<td><span class="badge bg-info text-dark"><?= esc((string) ($row['pay_mode'] ?? 'Cash')) ?></span></td>
|
||||
<td><?= esc((string) ($row['update_by'] ?? 'Staff')) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
|
@ -492,7 +492,7 @@ final class FhirGeneratorsTest extends CIUnitTestCase
|
|||
]);
|
||||
|
||||
$this->assertSame('HealthDocumentRecord', $output['hi_type']);
|
||||
$this->assertSame('DOC-4-2026-08-26', $output['care_context_reference']);
|
||||
$this->assertStringStartsWith('DOC-4-2026-08-26', $output['care_context_reference']);
|
||||
$this->assertTrue($output['validation']['valid']);
|
||||
$this->assertSame(100, $output['validation']['score']);
|
||||
|
||||
|
|
@ -502,4 +502,86 @@ final class FhirGeneratorsTest extends CIUnitTestCase
|
|||
$this->assertContains('Patient', $resourceTypes);
|
||||
$this->assertContains('DocumentReference', $resourceTypes);
|
||||
}
|
||||
|
||||
public function testHealthDocumentGeneratorSupportsCustomContentType(): void
|
||||
{
|
||||
$generator = $this->factory->healthDocument();
|
||||
$output = $generator->generate([
|
||||
'record_id' => '5',
|
||||
'visit_date' => '2026-08-31',
|
||||
'document_title' => 'Scanned X-Ray',
|
||||
'document_data_base64' => base64_encode('fake image data'),
|
||||
'content_type' => 'image/png',
|
||||
'patient' => [
|
||||
'id' => '11',
|
||||
'name' => 'DEVENDER SINGH',
|
||||
'abha_id' => '91510165305101',
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertSame('HealthDocumentRecord', $output['hi_type']);
|
||||
$bundle = $output['fhir_bundle'];
|
||||
$this->assertSame('Bundle', $bundle['resourceType']);
|
||||
|
||||
$docRef = null;
|
||||
foreach ($bundle['entry'] as $entry) {
|
||||
if (($entry['resource']['resourceType'] ?? '') === 'DocumentReference') {
|
||||
$docRef = $entry['resource'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
$this->assertNotNull($docRef);
|
||||
$this->assertSame('application/pdf', $docRef['content'][0]['attachment']['contentType']);
|
||||
}
|
||||
|
||||
public function testWellnessRecordGeneratorCreatesFullSections(): void
|
||||
{
|
||||
$generator = $this->factory->wellness();
|
||||
$output = $generator->generate([
|
||||
'record_id' => '101',
|
||||
'session_id' => '2',
|
||||
'visit_date' => '2026-09-01',
|
||||
'completed_at' => date(DATE_ATOM),
|
||||
'vitals' => [
|
||||
['loinc_code' => '8480-6', 'display' => 'Systolic blood pressure', 'value' => 120, 'unit' => 'mmHg', 'ucum_code' => 'mm[Hg]'],
|
||||
['loinc_code' => '8462-4', 'display' => 'Diastolic blood pressure', 'value' => 80, 'unit' => 'mmHg', 'ucum_code' => 'mm[Hg]'],
|
||||
['loinc_code' => '8867-4', 'display' => 'Heart rate', 'value' => 72, 'unit' => '/min', 'ucum_code' => '/min'],
|
||||
['loinc_code' => '8310-5', 'display' => 'Body temperature', 'value' => 36.8, 'unit' => 'Cel', 'ucum_code' => 'Cel'],
|
||||
['loinc_code' => '59408-5', 'display' => 'Oxygen saturation', 'value' => 98, 'unit' => '%', 'ucum_code' => '%'],
|
||||
],
|
||||
'physical_examination' => [
|
||||
'Complaints: Mild headache for 2 days',
|
||||
'Diagnosis: Essential hypertension (mild)',
|
||||
],
|
||||
'women_wellness' => [
|
||||
'lmp' => '2026-08-15',
|
||||
'gravida' => '2',
|
||||
'para' => '1',
|
||||
],
|
||||
'advice' => [
|
||||
'Smoking status: No',
|
||||
'Diet & Lifestyle Advice: Low salt diet, 30 min daily walk',
|
||||
],
|
||||
'patient' => [
|
||||
'id' => '11',
|
||||
'name' => 'SUNITA DEVI',
|
||||
'abha_id' => '91510165305101',
|
||||
'gender' => 'female',
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertSame('WellnessRecord', $output['hi_type']);
|
||||
$bundle = $output['fhir_bundle'];
|
||||
$this->assertSame('Bundle', $bundle['resourceType']);
|
||||
|
||||
$resourceTypes = array_column(array_column($bundle['entry'], 'resource'), 'resourceType');
|
||||
$this->assertContains('Composition', $resourceTypes);
|
||||
$this->assertContains('Patient', $resourceTypes);
|
||||
$this->assertContains('Observation', $resourceTypes);
|
||||
|
||||
$composition = $bundle['entry'][0]['resource'];
|
||||
$this->assertSame('https://nrces.in/ndhm/fhir/r4/StructureDefinition/WellnessRecord', $composition['meta']['profile'][0]);
|
||||
$this->assertGreaterThanOrEqual(1, count($composition['section']));
|
||||
$this->assertNotEmpty($composition['section'][0]['entry']);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user