feat(abdm): integrate Online/ABDM discharge template with PDF attachment in FHIR bundle
- Dynamically resolve and generate PDF using designated Online/ABDM template (is_abdm=1) from ipd_discharge_templates - Unify mPDF rendering logic between show_discharge and generateDischargeSummaryPdfBinary for exact byte-for-byte consistency - Attach generated PDF as DocumentReference under SNOMED 373942005 (Discharge summary) in NRCES FHIR DocumentBundle - Add quick-set actions and AJAX endpoints for Default and ABDM template selection in discharge template list UI - Update unit tests covering DocumentReference generation and template resolution
This commit is contained in:
parent
9b3d9ef6ee
commit
f3faa9c96e
|
|
@ -1069,6 +1069,8 @@ $routes->get('setting/template/discharge_template_get/(:num)', 'Setting\Template
|
||||||
$routes->match(['GET', 'POST'], 'setting/template/discharge_templates/delete/(:num)', 'Setting\Template::discharge_template_delete/$1');
|
$routes->match(['GET', 'POST'], 'setting/template/discharge_templates/delete/(:num)', 'Setting\Template::discharge_template_delete/$1');
|
||||||
$routes->get('Ipd_discharge/print_template_builder', 'Ipd_discharge::print_template_builder', ['filter' => 'permission:template.discharge,ipd_discharge.manage']);
|
$routes->get('Ipd_discharge/print_template_builder', 'Ipd_discharge::print_template_builder', ['filter' => 'permission:template.discharge,ipd_discharge.manage']);
|
||||||
$routes->post('Ipd_discharge/discharge_template_rename_ajax', 'Ipd_discharge::discharge_template_rename_ajax', ['filter' => 'permission:template.discharge,ipd_discharge.manage']);
|
$routes->post('Ipd_discharge/discharge_template_rename_ajax', 'Ipd_discharge::discharge_template_rename_ajax', ['filter' => 'permission:template.discharge,ipd_discharge.manage']);
|
||||||
|
$routes->post('Ipd_discharge/discharge_template_set_abdm_ajax', 'Ipd_discharge::discharge_template_set_abdm_ajax', ['filter' => 'permission:template.discharge,ipd_discharge.manage']);
|
||||||
|
$routes->post('Ipd_discharge/discharge_template_set_default_ajax', 'Ipd_discharge::discharge_template_set_default_ajax', ['filter' => 'permission:template.discharge,ipd_discharge.manage']);
|
||||||
$routes->match(['GET', 'POST'], 'setting/template/ipd_document_templates', 'Setting\Template::ipd_document_templates');
|
$routes->match(['GET', 'POST'], 'setting/template/ipd_document_templates', 'Setting\Template::ipd_document_templates');
|
||||||
$routes->get('setting/template/ipd_document_templates/delete/(:num)', 'Setting\Template::ipd_document_template_delete/$1');
|
$routes->get('setting/template/ipd_document_templates/delete/(:num)', 'Setting\Template::ipd_document_template_delete/$1');
|
||||||
$routes->match(['GET', 'POST'], 'setting/template/diagnosis_print_settings', 'Setting\Template::diagnosis_print_settings');
|
$routes->match(['GET', 'POST'], 'setting/template/diagnosis_print_settings', 'Setting\Template::diagnosis_print_settings');
|
||||||
|
|
|
||||||
|
|
@ -3064,6 +3064,28 @@ class AbdmGateway extends BaseController
|
||||||
$documents = $this->buildIpdPdfDocuments($ipdId, $dischargeRaw);
|
$documents = $this->buildIpdPdfDocuments($ipdId, $dischargeRaw);
|
||||||
$locationInfo = $this->resolveIpdLocation($ipdId);
|
$locationInfo = $this->resolveIpdLocation($ipdId);
|
||||||
|
|
||||||
|
$patientMobile = trim((string) ($patientRow['mphone1'] ?? $patientRow['p_mobile1'] ?? $ipdRow['P_mobile1'] ?? ''));
|
||||||
|
$patientAddress = trim((string) ($patientRow['p_add1'] ?? $patientRow['add1'] ?? $ipdRow['add1'] ?? ''));
|
||||||
|
$patientCity = trim((string) ($patientRow['p_city'] ?? $patientRow['city'] ?? $ipdRow['city'] ?? ''));
|
||||||
|
$patientState = trim((string) ($patientRow['p_state'] ?? $patientRow['state'] ?? $ipdRow['state'] ?? ''));
|
||||||
|
$patientPin = trim((string) ($patientRow['p_pin'] ?? $patientRow['pincode'] ?? ''));
|
||||||
|
$dischargeStatus = trim((string) ($ipdRow['discarge_patient_status'] ?? ''));
|
||||||
|
|
||||||
|
$courseRows = [];
|
||||||
|
foreach ($this->ipdRows('ipd_discharge_course', ['comp_report', 'comp_remark'], $ipdId) as $row) {
|
||||||
|
$cReport = trim((string) ($row['comp_report'] ?? ''));
|
||||||
|
$cRemark = trim((string) ($row['comp_remark'] ?? ''));
|
||||||
|
if ($cReport !== '') {
|
||||||
|
$courseRows[] = [
|
||||||
|
'text' => $cReport . ($cRemark !== '' ? (' - ' . $cRemark) : ''),
|
||||||
|
'name' => $cReport,
|
||||||
|
'remark' => $cRemark,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$courseRemarkRows = $this->ipdRows('ipd_discharge_course_remark', ['comp_remark'], $ipdId);
|
||||||
|
$courseRemark = trim((string) ($courseRemarkRows[0]['comp_remark'] ?? ''));
|
||||||
|
|
||||||
$uniqueSuffix = $isNewRecord ? ('-v' . time()) : '';
|
$uniqueSuffix = $isNewRecord ? ('-v' . time()) : '';
|
||||||
|
|
||||||
$source = [
|
$source = [
|
||||||
|
|
@ -3075,6 +3097,8 @@ class AbdmGateway extends BaseController
|
||||||
'completed_at' => $this->toIsoDateTimeOrNow($dischargeRaw),
|
'completed_at' => $this->toIsoDateTimeOrNow($dischargeRaw),
|
||||||
'department' => trim((string) ($ipdRow['dept_id'] ?? '')),
|
'department' => trim((string) ($ipdRow['dept_id'] ?? '')),
|
||||||
'doctor_name' => $doctorName,
|
'doctor_name' => $doctorName,
|
||||||
|
'discharge_status' => $dischargeStatus,
|
||||||
|
'problem' => trim((string) ($ipdRow['problem'] ?? '')),
|
||||||
'doctor' => [
|
'doctor' => [
|
||||||
'id' => $doctorId > 0 ? (string) $doctorId : '',
|
'id' => $doctorId > 0 ? (string) $doctorId : '',
|
||||||
'name' => $doctorName,
|
'name' => $doctorName,
|
||||||
|
|
@ -3091,6 +3115,13 @@ class AbdmGateway extends BaseController
|
||||||
($g = trim((string) ($patientRow['gender'] ?? ''))) !== '' ? $g : trim((string) ($patientRow['xgender'] ?? ''))
|
($g = trim((string) ($patientRow['gender'] ?? ''))) !== '' ? $g : trim((string) ($patientRow['xgender'] ?? ''))
|
||||||
),
|
),
|
||||||
'dob' => ! empty($patientRow['dob']) ? date('Y-m-d', strtotime((string) $patientRow['dob'])) : '',
|
'dob' => ! empty($patientRow['dob']) ? date('Y-m-d', strtotime((string) $patientRow['dob'])) : '',
|
||||||
|
'mobile' => $patientMobile,
|
||||||
|
'address' => [
|
||||||
|
'line' => $patientAddress,
|
||||||
|
'city' => $patientCity,
|
||||||
|
'state' => $patientState,
|
||||||
|
'pincode' => $patientPin,
|
||||||
|
],
|
||||||
'abha_id' => $abhaDigits,
|
'abha_id' => $abhaDigits,
|
||||||
'abha_address' => $abhaAddress,
|
'abha_address' => $abhaAddress,
|
||||||
],
|
],
|
||||||
|
|
@ -3100,6 +3131,7 @@ class AbdmGateway extends BaseController
|
||||||
'class_code' => 'IMP',
|
'class_code' => 'IMP',
|
||||||
'start' => $this->toIsoDateTimeOrNow($admissionRaw),
|
'start' => $this->toIsoDateTimeOrNow($admissionRaw),
|
||||||
'end' => $this->toIsoDateTimeOrNow($dischargeRaw),
|
'end' => $this->toIsoDateTimeOrNow($dischargeRaw),
|
||||||
|
'discharge_disposition' => $dischargeStatus,
|
||||||
'location_display' => (string) ($locationInfo['display'] ?? ''),
|
'location_display' => (string) ($locationInfo['display'] ?? ''),
|
||||||
'ward' => (string) ($locationInfo['ward'] ?? ''),
|
'ward' => (string) ($locationInfo['ward'] ?? ''),
|
||||||
'room' => (string) ($locationInfo['room'] ?? ''),
|
'room' => (string) ($locationInfo['room'] ?? ''),
|
||||||
|
|
@ -3114,8 +3146,10 @@ class AbdmGateway extends BaseController
|
||||||
'observations' => $observations,
|
'observations' => $observations,
|
||||||
'allergies' => $allergies,
|
'allergies' => $allergies,
|
||||||
'care_plans' => $carePlans,
|
'care_plans' => $carePlans,
|
||||||
'documents' => [],
|
'ui_course_treatment' => $courseRows,
|
||||||
'skip_pdf' => true,
|
'ui_course_treatment_narrative' => $courseRemark,
|
||||||
|
'documents' => $documents,
|
||||||
|
'template' => $this->resolveAbdmDischargeTemplate(),
|
||||||
];
|
];
|
||||||
|
|
||||||
$factory = new FhirGeneratorFactory();
|
$factory = new FhirGeneratorFactory();
|
||||||
|
|
@ -3250,46 +3284,33 @@ class AbdmGateway extends BaseController
|
||||||
$directory = WRITEPATH . 'uploads' . DIRECTORY_SEPARATOR . 'abdm' . DIRECTORY_SEPARATOR . 'ipd' . DIRECTORY_SEPARATOR . $ipdId;
|
$directory = WRITEPATH . 'uploads' . DIRECTORY_SEPARATOR . 'abdm' . DIRECTORY_SEPARATOR . 'ipd' . DIRECTORY_SEPARATOR . $ipdId;
|
||||||
$createdAt = $this->toIsoDateTimeOrNow($dischargeRaw);
|
$createdAt = $this->toIsoDateTimeOrNow($dischargeRaw);
|
||||||
|
|
||||||
// Ensure IPD Discharge Summary PDF is cached if missing
|
// Always generate fresh IPD Discharge Summary PDF using the configured Online/ABDM template
|
||||||
$summaryPath = $directory . DIRECTORY_SEPARATOR . 'discharge-summary.pdf';
|
$abdmTemplate = $this->resolveAbdmDischargeTemplate();
|
||||||
if (! is_file($summaryPath) || filesize($summaryPath) === 0) {
|
$abdmTemplateId = (int) ($abdmTemplate['id'] ?? 0);
|
||||||
try {
|
|
||||||
$dischargeCtrl = new \App\Controllers\Ipd_discharge();
|
|
||||||
$request = \Config\Services::request();
|
|
||||||
$response = \Config\Services::response();
|
|
||||||
$logger = \Config\Services::logger();
|
|
||||||
$dischargeCtrl->initController($request, $response, $logger);
|
|
||||||
$dischargeCtrl->generateDischargeSummaryPdfBinary($ipdId, true);
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
log_message('error', 'Auto-generating IPD Discharge Summary PDF failed for IPD {ipd}: {msg}', [
|
|
||||||
'ipd' => $ipdId,
|
|
||||||
'msg' => $e->getMessage(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure IPD Bill PDF is cached if missing
|
try {
|
||||||
$billPath = $directory . DIRECTORY_SEPARATOR . 'ipd-bill.pdf';
|
$dischargeCtrl = new \App\Controllers\Ipd_discharge();
|
||||||
if (! is_file($billPath) || filesize($billPath) === 0) {
|
$request = \Config\Services::request();
|
||||||
try {
|
$response = \Config\Services::response();
|
||||||
$billingCtrl = new \App\Controllers\Billing\Ipd();
|
$logger = \Config\Services::logger();
|
||||||
$request = \Config\Services::request();
|
$dischargeCtrl->initController($request, $response, $logger);
|
||||||
$response = \Config\Services::response();
|
$dischargeCtrl->generateDischargeSummaryPdfBinary($ipdId, true, $abdmTemplateId > 0 ? $abdmTemplateId : null);
|
||||||
$logger = \Config\Services::logger();
|
} catch (\Throwable $e) {
|
||||||
$billingCtrl->initController($request, $response, $logger);
|
log_message('error', 'Auto-generating IPD Discharge Summary PDF failed for IPD {ipd}: {msg}', [
|
||||||
$billingCtrl->generateIpdBillPdfBinary($ipdId, 1, true);
|
'ipd' => $ipdId,
|
||||||
} catch (\Throwable $e) {
|
'msg' => $e->getMessage(),
|
||||||
log_message('error', 'Auto-generating IPD Bill PDF failed for IPD {ipd}: {msg}', [
|
]);
|
||||||
'ipd' => $ipdId,
|
|
||||||
'msg' => $e->getMessage(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$documents = [];
|
$documents = [];
|
||||||
$definitions = [
|
$definitions = [
|
||||||
['file' => 'discharge-summary.pdf', 'title' => 'IPD Discharge Summary', 'loinc' => '18842-5', 'snomed' => '373942005'],
|
[
|
||||||
['file' => 'ipd-bill.pdf', 'title' => 'IPD Bill / Invoice', 'loinc' => '75490-3', 'snomed' => '823651000000106'],
|
'file' => 'discharge-summary.pdf',
|
||||||
|
'title' => 'IPD Discharge Summary',
|
||||||
|
'loinc' => '18842-5',
|
||||||
|
'snomed' => '373942005',
|
||||||
|
'snomed_display' => 'Discharge summary',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($definitions as $definition) {
|
foreach ($definitions as $definition) {
|
||||||
|
|
@ -3304,6 +3325,8 @@ class AbdmGateway extends BaseController
|
||||||
$documents[] = [
|
$documents[] = [
|
||||||
'title' => $definition['title'],
|
'title' => $definition['title'],
|
||||||
'loinc_code' => $definition['loinc'],
|
'loinc_code' => $definition['loinc'],
|
||||||
|
'snomed_code' => $definition['snomed'],
|
||||||
|
'snomed_display' => $definition['snomed_display'] ?? 'Discharge summary',
|
||||||
'content_type' => 'application/pdf',
|
'content_type' => 'application/pdf',
|
||||||
'data' => base64_encode($binary),
|
'data' => base64_encode($binary),
|
||||||
'created_at' => $createdAt,
|
'created_at' => $createdAt,
|
||||||
|
|
@ -3314,6 +3337,17 @@ class AbdmGateway extends BaseController
|
||||||
return $documents;
|
return $documents;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function resolveAbdmDischargeTemplate(): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$dischargeCtrl = new \App\Controllers\Ipd_discharge();
|
||||||
|
$dischargeCtrl->initController(\Config\Services::request(), \Config\Services::response(), \Config\Services::logger());
|
||||||
|
return $dischargeCtrl->resolveAbdmDischargeTemplate();
|
||||||
|
} catch (\Throwable) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<string,mixed>|null
|
* @return array<string,mixed>|null
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -315,7 +315,7 @@ class Ipd_discharge extends BaseController
|
||||||
->getResultArray();
|
->getResultArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
private function resolveAbdmDischargeTemplate(int $requestedId = 0): array
|
public function resolveAbdmDischargeTemplate(int $requestedId = 0): array
|
||||||
{
|
{
|
||||||
$templates = $this->getDischargeTemplateRows();
|
$templates = $this->getDischargeTemplateRows();
|
||||||
if (empty($templates)) {
|
if (empty($templates)) {
|
||||||
|
|
@ -5756,6 +5756,62 @@ class Ipd_discharge extends BaseController
|
||||||
'csrfName' => csrf_token(), 'csrfHash' => csrf_hash()]);
|
'csrfName' => csrf_token(), 'csrfHash' => csrf_hash()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function discharge_template_set_abdm_ajax()
|
||||||
|
{
|
||||||
|
if (! $this->request->isAJAX()) {
|
||||||
|
return $this->response->setStatusCode(400)->setJSON(['update' => 0]);
|
||||||
|
}
|
||||||
|
if ($resp = $this->requireAnyPermission(['template.discharge'])) {
|
||||||
|
return $this->response->setStatusCode(403)->setJSON(['update' => 0]);
|
||||||
|
}
|
||||||
|
$id = (int) ($this->request->getPost('id') ?? 0);
|
||||||
|
if ($id <= 0) {
|
||||||
|
return $this->response->setJSON([
|
||||||
|
'update' => 0,
|
||||||
|
'error_text' => 'Invalid template ID.',
|
||||||
|
'csrfName' => csrf_token(),
|
||||||
|
'csrfHash' => csrf_hash(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
$this->db->table('ipd_discharge_templates')->update(['is_abdm' => 0]);
|
||||||
|
$ok = (bool) $this->db->table('ipd_discharge_templates')->where('id', $id)->update(['is_abdm' => 1]);
|
||||||
|
|
||||||
|
return $this->response->setJSON([
|
||||||
|
'update' => $ok ? 1 : 0,
|
||||||
|
'error_text' => $ok ? 'Online / ABDM template updated.' : 'Unable to update ABDM template.',
|
||||||
|
'csrfName' => csrf_token(),
|
||||||
|
'csrfHash' => csrf_hash(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function discharge_template_set_default_ajax()
|
||||||
|
{
|
||||||
|
if (! $this->request->isAJAX()) {
|
||||||
|
return $this->response->setStatusCode(400)->setJSON(['update' => 0]);
|
||||||
|
}
|
||||||
|
if ($resp = $this->requireAnyPermission(['template.discharge'])) {
|
||||||
|
return $this->response->setStatusCode(403)->setJSON(['update' => 0]);
|
||||||
|
}
|
||||||
|
$id = (int) ($this->request->getPost('id') ?? 0);
|
||||||
|
if ($id <= 0) {
|
||||||
|
return $this->response->setJSON([
|
||||||
|
'update' => 0,
|
||||||
|
'error_text' => 'Invalid template ID.',
|
||||||
|
'csrfName' => csrf_token(),
|
||||||
|
'csrfHash' => csrf_hash(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
$this->db->table('ipd_discharge_templates')->update(['is_default' => 0]);
|
||||||
|
$ok = (bool) $this->db->table('ipd_discharge_templates')->where('id', $id)->update(['is_default' => 1]);
|
||||||
|
|
||||||
|
return $this->response->setJSON([
|
||||||
|
'update' => $ok ? 1 : 0,
|
||||||
|
'error_text' => $ok ? 'Default template updated.' : 'Unable to update default template.',
|
||||||
|
'csrfName' => csrf_token(),
|
||||||
|
'csrfHash' => csrf_hash(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
public function search_patient_ajax()
|
public function search_patient_ajax()
|
||||||
{
|
{
|
||||||
$permission = $this->requireAnyPermission([
|
$permission = $this->requireAnyPermission([
|
||||||
|
|
@ -8030,266 +8086,28 @@ class Ipd_discharge extends BaseController
|
||||||
|
|
||||||
$this->createIpdDischargeWorkTask($ipdId, $panelData);
|
$this->createIpdDischargeWorkTask($ipdId, $panelData);
|
||||||
|
|
||||||
try {
|
$pdfBinary = $this->generateDischargeSummaryPdfBinary($ipdId, $withHeader, $requestedTemplateId > 0 ? $requestedTemplateId : null);
|
||||||
$headerHtml = '';
|
|
||||||
if ($withHeader) {
|
|
||||||
$configuredHeader = $this->applyDischargeTemplateTokens(
|
|
||||||
trim((string) ($templateSettings['header_html'] ?? '')),
|
|
||||||
$templateTokenVars
|
|
||||||
);
|
|
||||||
$headerHtml = $this->sanitizeDischargePdfHtml($configuredHeader, false);
|
|
||||||
$headerHtml = mpdf_normalize_font_weight_css($headerHtml);
|
|
||||||
}
|
|
||||||
|
|
||||||
$configuredFooter = $this->applyDischargeTemplateTokens(
|
if (! is_string($pdfBinary) || $pdfBinary === '' || ! str_starts_with($pdfBinary, '%PDF-')) {
|
||||||
trim((string) ($templateSettings['footer_html'] ?? '')),
|
|
||||||
$templateTokenVars
|
|
||||||
);
|
|
||||||
$footerHtml = $this->sanitizeDischargePdfHtml(
|
|
||||||
$configuredFooter,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
$footerHtml = mpdf_normalize_font_weight_css($footerHtml);
|
|
||||||
if ($footerHtml === '') {
|
|
||||||
$footerHtml = '<div style="font-family:freeserif,serif;font-size:9pt;color:#6b7280;text-align:right;">Page {PAGENO}/{nbpg}</div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
$pdfHtml = $this->buildDischargePdfHtml($panelData, $renderedHtml, $withHeader, $templateName);
|
|
||||||
$pdfHtml = mpdf_normalize_font_weight_css($pdfHtml);
|
|
||||||
|
|
||||||
$mpdf = new Mpdf([
|
|
||||||
'mode' => 'utf-8',
|
|
||||||
'format' => ($templateSettings['page_size'] ?? 'A4') === 'CUSTOM'
|
|
||||||
? [max(20, (int) ($templateSettings['custom_width_mm'] ?? 210)), max(20, (int) ($templateSettings['custom_height_mm'] ?? 297))]
|
|
||||||
: (string) ($templateSettings['page_size'] ?? 'A4'),
|
|
||||||
'margin_left' => max(0, ((float) ($templateSettings['page_margin_left_cm'] ?? 0.8)) * 10),
|
|
||||||
'margin_right' => max(0, ((float) ($templateSettings['page_margin_right_cm'] ?? 0.8)) * 10),
|
|
||||||
'margin_top' => max(0, ((float) ($templateSettings['page_margin_top_cm'] ?? 0.8)) * 10),
|
|
||||||
'margin_bottom' => max(0, ((float) ($templateSettings['page_margin_bottom_cm'] ?? 0.8)) * 10),
|
|
||||||
'margin_header' => max(0, ((float) ($templateSettings['margin_header_cm'] ?? 0.5)) * 10),
|
|
||||||
'margin_footer' => max(0, ((float) ($templateSettings['margin_footer_cm'] ?? 0.5)) * 10),
|
|
||||||
'default_font' => 'freeserif',
|
|
||||||
'tempDir' => WRITEPATH . 'cache',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$mpdf->autoScriptToLang = true;
|
|
||||||
$mpdf->autoLangToFont = true;
|
|
||||||
$mpdf->SetTitle('Discharge Summary - ' . ($patientName !== '' ? $patientName : ('IPD ' . $ipdId)));
|
|
||||||
$mpdf->SetAuthor('Atria HMS');
|
|
||||||
|
|
||||||
if ($withHeader && $headerHtml !== '') {
|
|
||||||
$mpdf->SetHTMLHeader($headerHtml, 'O');
|
|
||||||
$mpdf->SetHTMLHeader($headerHtml, 'E');
|
|
||||||
}
|
|
||||||
if ($footerHtml !== '') {
|
|
||||||
$mpdf->SetHTMLFooter($footerHtml, 'O');
|
|
||||||
$mpdf->SetHTMLFooter($footerHtml, 'E');
|
|
||||||
}
|
|
||||||
|
|
||||||
$watermarkType = (string) ($templateSettings['watermark_type'] ?? 'none');
|
|
||||||
$watermarkAlpha = max(0.01, min(1.00, (float) ($templateSettings['watermark_alpha'] ?? 0.12)));
|
|
||||||
if ($watermarkType === 'text') {
|
|
||||||
$watermarkText = trim((string) ($templateSettings['watermark_text'] ?? ''));
|
|
||||||
if ($watermarkText !== '') {
|
|
||||||
$mpdf->SetWatermarkText($watermarkText, $watermarkAlpha);
|
|
||||||
$mpdf->showWatermarkText = true;
|
|
||||||
}
|
|
||||||
} elseif ($watermarkType === 'image') {
|
|
||||||
$watermarkImage = trim((string) ($templateSettings['watermark_image'] ?? ''));
|
|
||||||
$watermarkPath = $watermarkImage !== '' ? FCPATH . ltrim(str_replace('\\', '/', $watermarkImage), '/') : '';
|
|
||||||
if ($watermarkPath !== '' && is_file($watermarkPath)) {
|
|
||||||
$mpdf->SetWatermarkImage($watermarkPath, $watermarkAlpha);
|
|
||||||
$mpdf->showWatermarkImage = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($withHeader || $footerHtml !== '') {
|
|
||||||
$mpdf->AddPage();
|
|
||||||
if ($withHeader && $headerHtml !== '') {
|
|
||||||
$mpdf->SetHTMLHeader($headerHtml, 'O', true);
|
|
||||||
}
|
|
||||||
if ($footerHtml !== '') {
|
|
||||||
$mpdf->SetHTMLFooter($footerHtml, 'O', true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$pdfHtml = '';
|
|
||||||
$templateCss = trim((string) ($templateSettings['template_css'] ?? ''));
|
|
||||||
if ($templateCss !== '') {
|
|
||||||
$templateCss = (string) preg_replace('/@page(?:\s+[^{]+)?\s*\{[^{}]*\}/i', '', $templateCss);
|
|
||||||
$pdfHtml .= '<style>' . "\n" . $templateCss . "\n" . '</style>' . "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
$pdfHtml .= $renderedHtml;
|
|
||||||
$pdfHtml = mpdf_normalize_font_weight_css($pdfHtml);
|
|
||||||
|
|
||||||
$debugDirectory = WRITEPATH . 'debug';
|
|
||||||
if (! is_dir($debugDirectory)) {
|
|
||||||
mkdir($debugDirectory, 0755, true);
|
|
||||||
}
|
|
||||||
$debugTemplateId = (int) ($templatePack['selected_template_id'] ?? 0);
|
|
||||||
file_put_contents($debugDirectory . DIRECTORY_SEPARATOR . 'discharge_mpdf_input_' . $ipdId . '_' . $debugTemplateId . '.html', $pdfHtml, LOCK_EX);
|
|
||||||
file_put_contents($debugDirectory . DIRECTORY_SEPARATOR . 'discharge_mpdf_runtime_' . $ipdId . '_' . $debugTemplateId . '.json', json_encode([
|
|
||||||
'header_html' => $headerHtml,
|
|
||||||
'footer_html' => $footerHtml,
|
|
||||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), LOCK_EX);
|
|
||||||
|
|
||||||
$pdfBinary = $this->runMpdfWithTolerantWarnings(static function () use ($mpdf, $pdfHtml, $fileName): string {
|
|
||||||
$mpdf->WriteHTML($pdfHtml);
|
|
||||||
|
|
||||||
return $mpdf->Output($fileName, Destination::STRING_RETURN);
|
|
||||||
});
|
|
||||||
$this->cacheAbdmIpdPdf($ipdId, 'discharge-summary.pdf', $pdfBinary);
|
|
||||||
|
|
||||||
// Clear any output buffers to prevent corruption of binary PDF data
|
|
||||||
while (ob_get_level() > 0) {
|
|
||||||
ob_end_clean();
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->response
|
return $this->response
|
||||||
->setHeader('Content-Type', 'application/pdf')
|
->setStatusCode(500)
|
||||||
->setHeader('Content-Length', (string) strlen($pdfBinary))
|
->setHeader('Content-Type', 'text/plain; charset=UTF-8')
|
||||||
->setHeader('Content-Disposition', 'inline; filename="' . rawurlencode($fileName) . '"; filename*=UTF-8\'\''. rawurlencode($fileName))
|
->setBody('Unable to generate discharge PDF due to invalid template content. Please edit template CSS/HTML and retry.');
|
||||||
->setHeader('Cache-Control', 'private, max-age=0, must-revalidate')
|
|
||||||
->setHeader('Accept-Ranges', 'bytes')
|
|
||||||
->setHeader('X-Content-Type-Options', 'nosniff')
|
|
||||||
->setBody($pdfBinary);
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
log_message('error', 'PDF generation failed for IPD {ipd}: {msg}', [
|
|
||||||
'ipd' => $ipdId,
|
|
||||||
'msg' => $e->getMessage(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if ($printType !== 0) {
|
|
||||||
try {
|
|
||||||
$safeBodySource = trim($renderedHtml) !== '' ? $renderedHtml : (string) $content;
|
|
||||||
$safeBody = $this->sanitizeDischargePdfHtml($safeBodySource);
|
|
||||||
$safeBody = (string) preg_replace('/<style\b[^>]*>[\s\S]*?<\/style>/i', '', $safeBody);
|
|
||||||
$safeBody = (string) preg_replace('/<script\b[^>]*>[\s\S]*?<\/script>/i', '', $safeBody);
|
|
||||||
$safePdfHtml = $this->buildDischargePdfHtml($panelData, $safeBody, false, $templateName);
|
|
||||||
$safePdfHtml = mpdf_normalize_font_weight_css($safePdfHtml);
|
|
||||||
|
|
||||||
$fallbackPdf = new Mpdf([
|
|
||||||
'mode' => 'utf-8',
|
|
||||||
'format' => 'A4',
|
|
||||||
'margin_left' => 8,
|
|
||||||
'margin_right' => 8,
|
|
||||||
'margin_top' => 8,
|
|
||||||
'margin_bottom' => 8,
|
|
||||||
'margin_header' => 5,
|
|
||||||
'margin_footer' => 5,
|
|
||||||
'default_font' => 'freeserif',
|
|
||||||
'tempDir' => WRITEPATH . 'cache',
|
|
||||||
]);
|
|
||||||
$fallbackPdf->autoScriptToLang = true;
|
|
||||||
$fallbackPdf->autoLangToFont = true;
|
|
||||||
$fallbackPdf->SetTitle('Discharge Summary - ' . ($patientName !== '' ? $patientName : ('IPD ' . $ipdId)));
|
|
||||||
$fallbackPdf->SetAuthor('Atria HMS');
|
|
||||||
$fallbackPdf->SetHTMLFooter('<div style="font-family:freeserif,serif;font-size:9pt;color:#6b7280;text-align:right;">Page {PAGENO}/{nbpg}</div>');
|
|
||||||
$pdfBinary = $this->runMpdfWithTolerantWarnings(static function () use ($fallbackPdf, $safePdfHtml, $fileName): string {
|
|
||||||
$fallbackPdf->WriteHTML($safePdfHtml);
|
|
||||||
|
|
||||||
return $fallbackPdf->Output($fileName, Destination::STRING_RETURN);
|
|
||||||
});
|
|
||||||
|
|
||||||
log_message('warning', 'Discharge PDF fallback render succeeded for IPD {ipd}', ['ipd' => $ipdId]);
|
|
||||||
|
|
||||||
// Clear any output buffers to prevent corruption of binary PDF data
|
|
||||||
while (ob_get_level() > 0) {
|
|
||||||
ob_end_clean();
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->response
|
|
||||||
->setHeader('Content-Type', 'application/pdf')
|
|
||||||
->setHeader('Content-Length', (string) strlen($pdfBinary))
|
|
||||||
->setHeader('Content-Disposition', 'inline; filename="' . rawurlencode($fileName) . '"; filename*=UTF-8\'\''. rawurlencode($fileName))
|
|
||||||
->setHeader('Cache-Control', 'private, max-age=0, must-revalidate')
|
|
||||||
->setHeader('Accept-Ranges', 'bytes')
|
|
||||||
->setHeader('X-Content-Type-Options', 'nosniff')
|
|
||||||
->setBody($pdfBinary);
|
|
||||||
} catch (\Throwable $fallbackError) {
|
|
||||||
log_message('error', 'Discharge PDF fallback failed for IPD {ipd}: {msg}', [
|
|
||||||
'ipd' => $ipdId,
|
|
||||||
'msg' => $fallbackError->getMessage(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
try {
|
|
||||||
$plainSourceHtml = trim($renderedHtml) !== '' ? $renderedHtml : (string) $content;
|
|
||||||
$plainSourceHtml = (string) preg_replace('/<style\b[^>]*>[\s\S]*?<\/style>/i', '', $plainSourceHtml);
|
|
||||||
$plainSourceHtml = (string) preg_replace('/<script\b[^>]*>[\s\S]*?<\/script>/i', '', $plainSourceHtml);
|
|
||||||
$plainSource = strip_tags($plainSourceHtml);
|
|
||||||
$plainSource = $this->forceValidUtf8($plainSource);
|
|
||||||
$plainSource = preg_replace('/\R{3,}/', "\n\n", (string) $plainSource) ?? '';
|
|
||||||
|
|
||||||
$plainHtml = '<!doctype html><html><head><meta charset="utf-8"></head><body>'
|
|
||||||
. '<h3 style="font-family:freeserif,serif;margin:0 0 8px 0;">Discharge Summary</h3>'
|
|
||||||
. '<pre style="font-family:freeserif,serif;font-size:11pt;white-space:pre-wrap;line-height:1.4;">'
|
|
||||||
. htmlspecialchars($plainSource, ENT_QUOTES, 'UTF-8')
|
|
||||||
. '</pre></body></html>';
|
|
||||||
|
|
||||||
$ultimatePdf = new Mpdf([
|
|
||||||
'mode' => 'utf-8',
|
|
||||||
'format' => 'A4',
|
|
||||||
'margin_left' => 8,
|
|
||||||
'margin_right' => 8,
|
|
||||||
'margin_top' => 8,
|
|
||||||
'margin_bottom' => 8,
|
|
||||||
'margin_header' => 5,
|
|
||||||
'margin_footer' => 5,
|
|
||||||
'default_font' => 'freeserif',
|
|
||||||
'tempDir' => WRITEPATH . 'cache',
|
|
||||||
]);
|
|
||||||
$ultimatePdf->autoScriptToLang = true;
|
|
||||||
$ultimatePdf->autoLangToFont = true;
|
|
||||||
$ultimatePdf->SetTitle('Discharge Summary - ' . ($patientName !== '' ? $patientName : ('IPD ' . $ipdId)));
|
|
||||||
$ultimatePdf->SetAuthor('Atria HMS');
|
|
||||||
$ultimatePdf->SetHTMLFooter('<div style="font-family:freeserif,serif;font-size:9pt;color:#6b7280;text-align:right;">Page {PAGENO}/{nbpg}</div>');
|
|
||||||
$pdfBinary = $this->runMpdfWithTolerantWarnings(static function () use ($ultimatePdf, $plainHtml, $fileName): string {
|
|
||||||
$ultimatePdf->WriteHTML($plainHtml);
|
|
||||||
|
|
||||||
return $ultimatePdf->Output($fileName, Destination::STRING_RETURN);
|
|
||||||
});
|
|
||||||
|
|
||||||
log_message('warning', 'Discharge PDF ultimate fallback render succeeded for IPD {ipd}', ['ipd' => $ipdId]);
|
|
||||||
|
|
||||||
// Clear any output buffers to prevent corruption of binary PDF data
|
|
||||||
while (ob_get_level() > 0) {
|
|
||||||
ob_end_clean();
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->response
|
|
||||||
->setHeader('Content-Type', 'application/pdf')
|
|
||||||
->setHeader('Content-Length', (string) strlen($pdfBinary))
|
|
||||||
->setHeader('Content-Disposition', 'inline; filename="' . rawurlencode($fileName) . '"; filename*=UTF-8\'\''. rawurlencode($fileName))
|
|
||||||
->setHeader('Cache-Control', 'private, max-age=0, must-revalidate')
|
|
||||||
->setHeader('Accept-Ranges', 'bytes')
|
|
||||||
->setHeader('X-Content-Type-Options', 'nosniff')
|
|
||||||
->setBody($pdfBinary);
|
|
||||||
} catch (\Throwable $ultimateError) {
|
|
||||||
log_message('error', 'Discharge PDF ultimate fallback failed for IPD {ipd}: {msg}', [
|
|
||||||
'ipd' => $ipdId,
|
|
||||||
'msg' => $ultimateError->getMessage(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return $this->response
|
|
||||||
->setStatusCode(500)
|
|
||||||
->setHeader('Content-Type', 'text/plain; charset=UTF-8')
|
|
||||||
->setBody('Unable to generate discharge PDF due to invalid template content. Please edit template CSS/HTML and retry.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return view('billing/ipd/discharge_print', [
|
// Clear any output buffers to prevent corruption of binary PDF data
|
||||||
'ipd_id' => $ipdId,
|
while (ob_get_level() > 0) {
|
||||||
'ipd_info' => $panelData['ipd_info'] ?? null,
|
ob_end_clean();
|
||||||
'person_info' => $panelData['person_info'] ?? null,
|
}
|
||||||
'content' => $renderedHtml,
|
|
||||||
'selected_template_name' => (string) ($templatePack['selected_template_name'] ?? ''),
|
return $this->response
|
||||||
'selected_template_id' => (int) ($templatePack['selected_template_id'] ?? 0),
|
->setHeader('Content-Type', 'application/pdf')
|
||||||
'print_type' => $printType,
|
->setHeader('Content-Length', (string) strlen($pdfBinary))
|
||||||
'print_mode' => 'standard',
|
->setHeader('Content-Disposition', 'inline; filename="' . rawurlencode($fileName) . '"; filename*=UTF-8\'\''. rawurlencode($fileName))
|
||||||
]);
|
->setHeader('Cache-Control', 'private, max-age=0, must-revalidate')
|
||||||
|
->setHeader('Accept-Ranges', 'bytes')
|
||||||
|
->setHeader('X-Content-Type-Options', 'nosniff')
|
||||||
|
->setBody($pdfBinary);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function sanitizeDischargePdfHtml(string $html, bool $stripNamedHeaderFooterWrappers = true): string
|
private function sanitizeDischargePdfHtml(string $html, bool $stripNamedHeaderFooterWrappers = true): string
|
||||||
|
|
@ -8694,7 +8512,7 @@ class Ipd_discharge extends BaseController
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function generateDischargeSummaryPdfBinary(int $ipdId, bool $withHeader = true): ?string
|
public function generateDischargeSummaryPdfBinary(int $ipdId, bool $withHeader = true, ?int $templateId = null): ?string
|
||||||
{
|
{
|
||||||
if ($ipdId <= 0) {
|
if ($ipdId <= 0) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -8713,7 +8531,12 @@ class Ipd_discharge extends BaseController
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$templatePack = $this->applyDischargeTemplate($content, $panelData, null);
|
if ($templateId === null || $templateId <= 0) {
|
||||||
|
$abdmTemplate = $this->resolveAbdmDischargeTemplate(0);
|
||||||
|
$templateId = (int) ($abdmTemplate['id'] ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
$templatePack = $this->applyDischargeTemplate($content, $panelData, $templateId > 0 ? $templateId : null);
|
||||||
$templateSettings = is_array($templatePack['selected_template_settings'] ?? null)
|
$templateSettings = is_array($templatePack['selected_template_settings'] ?? null)
|
||||||
? $templatePack['selected_template_settings']
|
? $templatePack['selected_template_settings']
|
||||||
: $this->defaultDischargeTemplateSettings();
|
: $this->defaultDischargeTemplateSettings();
|
||||||
|
|
@ -8745,15 +8568,15 @@ class Ipd_discharge extends BaseController
|
||||||
trim((string) ($templateSettings['footer_html'] ?? '')),
|
trim((string) ($templateSettings['footer_html'] ?? '')),
|
||||||
$templateTokenVars
|
$templateTokenVars
|
||||||
);
|
);
|
||||||
$footerHtml = $this->sanitizeDischargePdfHtml($configuredFooter, false);
|
$footerHtml = $this->sanitizeDischargePdfHtml(
|
||||||
|
$configuredFooter,
|
||||||
|
false
|
||||||
|
);
|
||||||
$footerHtml = mpdf_normalize_font_weight_css($footerHtml);
|
$footerHtml = mpdf_normalize_font_weight_css($footerHtml);
|
||||||
if ($footerHtml === '') {
|
if ($footerHtml === '') {
|
||||||
$footerHtml = '<div style="font-family:freeserif,serif;font-size:9pt;color:#6b7280;text-align:right;">Page {PAGENO}/{nbpg}</div>';
|
$footerHtml = '<div style="font-family:freeserif,serif;font-size:9pt;color:#6b7280;text-align:right;">Page {PAGENO}/{nbpg}</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
$pdfHtml = $this->buildDischargePdfHtml($panelData, $renderedHtml, $withHeader, $templateName);
|
|
||||||
$pdfHtml = mpdf_normalize_font_weight_css($pdfHtml);
|
|
||||||
|
|
||||||
$mpdf = new Mpdf([
|
$mpdf = new Mpdf([
|
||||||
'mode' => 'utf-8',
|
'mode' => 'utf-8',
|
||||||
'format' => ($templateSettings['page_size'] ?? 'A4') === 'CUSTOM'
|
'format' => ($templateSettings['page_size'] ?? 'A4') === 'CUSTOM'
|
||||||
|
|
@ -8783,12 +8606,54 @@ class Ipd_discharge extends BaseController
|
||||||
$mpdf->SetHTMLFooter($footerHtml, 'E');
|
$mpdf->SetHTMLFooter($footerHtml, 'E');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$watermarkType = (string) ($templateSettings['watermark_type'] ?? 'none');
|
||||||
|
$watermarkAlpha = max(0.01, min(1.00, (float) ($templateSettings['watermark_alpha'] ?? 0.12)));
|
||||||
|
if ($watermarkType === 'text') {
|
||||||
|
$watermarkText = trim((string) ($templateSettings['watermark_text'] ?? ''));
|
||||||
|
if ($watermarkText !== '') {
|
||||||
|
$mpdf->SetWatermarkText($watermarkText, $watermarkAlpha);
|
||||||
|
$mpdf->showWatermarkText = true;
|
||||||
|
}
|
||||||
|
} elseif ($watermarkType === 'image') {
|
||||||
|
$watermarkImage = trim((string) ($templateSettings['watermark_image'] ?? ''));
|
||||||
|
$watermarkPath = $watermarkImage !== '' ? FCPATH . ltrim(str_replace('\\', '/', $watermarkImage), '/') : '';
|
||||||
|
if ($watermarkPath !== '' && is_file($watermarkPath)) {
|
||||||
|
$mpdf->SetWatermarkImage($watermarkPath, $watermarkAlpha);
|
||||||
|
$mpdf->showWatermarkImage = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($withHeader || $footerHtml !== '') {
|
||||||
|
$mpdf->AddPage();
|
||||||
|
if ($withHeader && $headerHtml !== '') {
|
||||||
|
$mpdf->SetHTMLHeader($headerHtml, 'O', true);
|
||||||
|
}
|
||||||
|
if ($footerHtml !== '') {
|
||||||
|
$mpdf->SetHTMLFooter($footerHtml, 'O', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdfHtml = '';
|
||||||
$templateCss = trim((string) ($templateSettings['template_css'] ?? ''));
|
$templateCss = trim((string) ($templateSettings['template_css'] ?? ''));
|
||||||
if ($templateCss !== '') {
|
if ($templateCss !== '') {
|
||||||
$templateCss = (string) preg_replace('/@page(?:\s+[^{]+)?\s*\{[^{}]*\}/i', '', $templateCss);
|
$templateCss = (string) preg_replace('/@page(?:\s+[^{]+)?\s*\{[^{}]*\}/i', '', $templateCss);
|
||||||
$pdfHtml = '<style>' . "\n" . $templateCss . "\n" . '</style>' . "\n" . $pdfHtml;
|
$pdfHtml .= '<style>' . "\n" . $templateCss . "\n" . '</style>' . "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$pdfHtml .= $renderedHtml;
|
||||||
|
$pdfHtml = mpdf_normalize_font_weight_css($pdfHtml);
|
||||||
|
|
||||||
|
$debugDirectory = WRITEPATH . 'debug';
|
||||||
|
if (! is_dir($debugDirectory)) {
|
||||||
|
mkdir($debugDirectory, 0755, true);
|
||||||
|
}
|
||||||
|
$debugTemplateId = (int) ($templatePack['selected_template_id'] ?? 0);
|
||||||
|
file_put_contents($debugDirectory . DIRECTORY_SEPARATOR . 'discharge_mpdf_input_' . $ipdId . '_' . $debugTemplateId . '.html', $pdfHtml, LOCK_EX);
|
||||||
|
file_put_contents($debugDirectory . DIRECTORY_SEPARATOR . 'discharge_mpdf_runtime_' . $ipdId . '_' . $debugTemplateId . '.json', json_encode([
|
||||||
|
'header_html' => $headerHtml,
|
||||||
|
'footer_html' => $footerHtml,
|
||||||
|
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), LOCK_EX);
|
||||||
|
|
||||||
$pdfBinary = $this->runMpdfWithTolerantWarnings(static function () use ($mpdf, $pdfHtml, $fileName): string {
|
$pdfBinary = $this->runMpdfWithTolerantWarnings(static function () use ($mpdf, $pdfHtml, $fileName): string {
|
||||||
$mpdf->WriteHTML($pdfHtml);
|
$mpdf->WriteHTML($pdfHtml);
|
||||||
|
|
||||||
|
|
@ -8814,41 +8679,28 @@ class Ipd_discharge extends BaseController
|
||||||
$directory = WRITEPATH . 'uploads' . DIRECTORY_SEPARATOR . 'abdm' . DIRECTORY_SEPARATOR . 'ipd' . DIRECTORY_SEPARATOR . $ipdId;
|
$directory = WRITEPATH . 'uploads' . DIRECTORY_SEPARATOR . 'abdm' . DIRECTORY_SEPARATOR . 'ipd' . DIRECTORY_SEPARATOR . $ipdId;
|
||||||
$createdAt = $this->toIsoDateTimeOrNow($dischargeRaw);
|
$createdAt = $this->toIsoDateTimeOrNow($dischargeRaw);
|
||||||
|
|
||||||
// Ensure IPD Discharge Summary PDF is cached if missing
|
// Always generate fresh IPD Discharge Summary PDF using the configured Online/ABDM template
|
||||||
$summaryPath = $directory . DIRECTORY_SEPARATOR . 'discharge-summary.pdf';
|
$abdmTemplate = $this->resolveAbdmDischargeTemplate();
|
||||||
if (! is_file($summaryPath) || filesize($summaryPath) === 0) {
|
$abdmTemplateId = (int) ($abdmTemplate['id'] ?? 0);
|
||||||
try {
|
|
||||||
$this->generateDischargeSummaryPdfBinary($ipdId, true);
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
log_message('error', 'Auto-generating IPD Discharge Summary PDF failed for IPD {ipd}: {msg}', [
|
|
||||||
'ipd' => $ipdId,
|
|
||||||
'msg' => $e->getMessage(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure IPD Bill PDF is cached if missing
|
try {
|
||||||
$billPath = $directory . DIRECTORY_SEPARATOR . 'ipd-bill.pdf';
|
$this->generateDischargeSummaryPdfBinary($ipdId, true, $abdmTemplateId > 0 ? $abdmTemplateId : null);
|
||||||
if (! is_file($billPath) || filesize($billPath) === 0) {
|
} catch (\Throwable $e) {
|
||||||
try {
|
log_message('error', 'Auto-generating IPD Discharge Summary PDF failed for IPD {ipd}: {msg}', [
|
||||||
$billingCtrl = new \App\Controllers\Billing\Ipd();
|
'ipd' => $ipdId,
|
||||||
$request = \Config\Services::request();
|
'msg' => $e->getMessage(),
|
||||||
$response = \Config\Services::response();
|
]);
|
||||||
$logger = \Config\Services::logger();
|
|
||||||
$billingCtrl->initController($request, $response, $logger);
|
|
||||||
$billingCtrl->generateIpdBillPdfBinary($ipdId, 1, true);
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
log_message('error', 'Auto-generating IPD Bill PDF failed for IPD {ipd}: {msg}', [
|
|
||||||
'ipd' => $ipdId,
|
|
||||||
'msg' => $e->getMessage(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$documents = [];
|
$documents = [];
|
||||||
$definitions = [
|
$definitions = [
|
||||||
['file' => 'discharge-summary.pdf', 'title' => 'IPD Discharge Summary', 'loinc' => '18842-5', 'snomed' => '373942005'],
|
[
|
||||||
['file' => 'ipd-bill.pdf', 'title' => 'IPD Bill / Invoice', 'loinc' => '75490-3', 'snomed' => '823651000000106'],
|
'file' => 'discharge-summary.pdf',
|
||||||
|
'title' => 'IPD Discharge Summary',
|
||||||
|
'loinc' => '18842-5',
|
||||||
|
'snomed' => '373942005',
|
||||||
|
'snomed_display' => 'Discharge summary',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($definitions as $definition) {
|
foreach ($definitions as $definition) {
|
||||||
|
|
@ -8864,6 +8716,7 @@ class Ipd_discharge extends BaseController
|
||||||
'title' => $definition['title'],
|
'title' => $definition['title'],
|
||||||
'loinc_code' => $definition['loinc'],
|
'loinc_code' => $definition['loinc'],
|
||||||
'snomed_code' => $definition['snomed'],
|
'snomed_code' => $definition['snomed'],
|
||||||
|
'snomed_display' => $definition['snomed_display'] ?? 'Discharge summary',
|
||||||
'content_type' => 'application/pdf',
|
'content_type' => 'application/pdf',
|
||||||
'data' => base64_encode($binary),
|
'data' => base64_encode($binary),
|
||||||
'created_at' => $createdAt,
|
'created_at' => $createdAt,
|
||||||
|
|
@ -9184,13 +9037,25 @@ class Ipd_discharge extends BaseController
|
||||||
$abhaAddress = $abhaIdRaw;
|
$abhaAddress = $abhaIdRaw;
|
||||||
}
|
}
|
||||||
|
|
||||||
$admissionRaw = trim((string) ($ipdRow['register_date'] ?? ''));
|
$admissionDateRaw = trim((string) ($ipdRow['register_date'] ?? ''));
|
||||||
$dischargeRaw = trim((string) ($ipdRow['discharge_date'] ?? ''));
|
$admissionTimeRaw = trim((string) ($ipdRow['reg_time'] ?? ''));
|
||||||
$admissionIso = $this->toIsoDateTimeOrNow($admissionRaw);
|
$admissionCombined = $admissionDateRaw;
|
||||||
$dischargeIso = $this->toIsoDateTimeOrNow($dischargeRaw);
|
if ($admissionCombined !== '' && $admissionTimeRaw !== '') {
|
||||||
$visitDate = $dischargeRaw !== ''
|
$admissionCombined .= ' ' . $admissionTimeRaw;
|
||||||
? date('Y-m-d', strtotime($dischargeRaw))
|
}
|
||||||
: ($admissionRaw !== '' ? date('Y-m-d', strtotime($admissionRaw)) : date('Y-m-d'));
|
|
||||||
|
$dischargeDateRaw = trim((string) ($ipdRow['discharge_date'] ?? ''));
|
||||||
|
$dischargeTimeRaw = trim((string) ($ipdRow['discharge_time'] ?? ''));
|
||||||
|
$dischargeCombined = $dischargeDateRaw;
|
||||||
|
if ($dischargeCombined !== '' && $dischargeTimeRaw !== '') {
|
||||||
|
$dischargeCombined .= ' ' . $dischargeTimeRaw;
|
||||||
|
}
|
||||||
|
|
||||||
|
$admissionIso = $this->toIsoDateTimeOrNow($admissionCombined !== '' ? $admissionCombined : $admissionDateRaw);
|
||||||
|
$dischargeIso = $this->toIsoDateTimeOrNow($dischargeCombined !== '' ? $dischargeCombined : $dischargeDateRaw);
|
||||||
|
$visitDate = $dischargeDateRaw !== ''
|
||||||
|
? date('Y-m-d', strtotime($dischargeDateRaw))
|
||||||
|
: ($admissionDateRaw !== '' ? date('Y-m-d', strtotime($admissionDateRaw)) : date('Y-m-d'));
|
||||||
|
|
||||||
$diagnosisRows = $this->byIpdRows('ipd_discharge_diagnosis', ['comp_report'], 'id ASC', $ipdId);
|
$diagnosisRows = $this->byIpdRows('ipd_discharge_diagnosis', ['comp_report'], 'id ASC', $ipdId);
|
||||||
$conditionRows = [];
|
$conditionRows = [];
|
||||||
|
|
@ -9200,6 +9065,12 @@ class Ipd_discharge extends BaseController
|
||||||
$conditionRows[] = ['text' => $text, 'code' => ''];
|
$conditionRows[] = ['text' => $text, 'code' => ''];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (empty($conditionRows) && ! empty($ipdRow['problem'])) {
|
||||||
|
$prob = trim((string) $ipdRow['problem']);
|
||||||
|
if ($this->isMeaningfulDischargeValue($prob)) {
|
||||||
|
$conditionRows[] = ['text' => $prob, 'code' => ''];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$chiefComplaintsList = [];
|
$chiefComplaintsList = [];
|
||||||
if ($this->tableHasColumns('ipd_discharge_complaint', ['ipd_id', 'comp_report'])) {
|
if ($this->tableHasColumns('ipd_discharge_complaint', ['ipd_id', 'comp_report'])) {
|
||||||
|
|
@ -9446,6 +9317,27 @@ class Ipd_discharge extends BaseController
|
||||||
}
|
}
|
||||||
$uiClinicalHistoryText = implode("\n", $historyList);
|
$uiClinicalHistoryText = implode("\n", $historyList);
|
||||||
|
|
||||||
|
$courseRows = [];
|
||||||
|
if ($this->tableHasColumns('ipd_discharge_course', ['ipd_id', 'comp_report'])) {
|
||||||
|
$cRows = $this->byIpdRows('ipd_discharge_course', ['comp_report', 'comp_remark'], 'id ASC', $ipdId);
|
||||||
|
foreach ($cRows as $row) {
|
||||||
|
$cReport = trim((string) ($row['comp_report'] ?? ''));
|
||||||
|
$cRemark = trim((string) ($row['comp_remark'] ?? ''));
|
||||||
|
if ($cReport !== '') {
|
||||||
|
$courseRows[] = [
|
||||||
|
'text' => $cReport . ($cRemark !== '' ? (' - ' . $cRemark) : ''),
|
||||||
|
'name' => $cReport,
|
||||||
|
'remark' => $cRemark,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$courseRemark = '';
|
||||||
|
if ($this->tableHasColumns('ipd_discharge_course_remark', ['ipd_id', 'comp_remark'])) {
|
||||||
|
$crRow = $this->firstRowByIpd('ipd_discharge_course_remark', $ipdId);
|
||||||
|
$courseRemark = trim((string) ($crRow['comp_remark'] ?? ''));
|
||||||
|
}
|
||||||
|
|
||||||
$documentsList = $this->buildIpdPdfDocumentsList($ipdId, $dischargeIso);
|
$documentsList = $this->buildIpdPdfDocumentsList($ipdId, $dischargeIso);
|
||||||
$locationInfo = $this->resolveIpdLocation($ipdId);
|
$locationInfo = $this->resolveIpdLocation($ipdId);
|
||||||
|
|
||||||
|
|
@ -9462,6 +9354,13 @@ class Ipd_discharge extends BaseController
|
||||||
$uhidNo = trim((string) ($patientRow['uhid_no'] ?? $patientRow['uhid'] ?? $patientRow['patient_code'] ?? $patientRow['p_id'] ?? $patientId));
|
$uhidNo = trim((string) ($patientRow['uhid_no'] ?? $patientRow['uhid'] ?? $patientRow['patient_code'] ?? $patientRow['p_id'] ?? $patientId));
|
||||||
$ipdNo = trim((string) ($ipdRow['ipd_no'] ?? $ipdRow['ipd_code'] ?? ('IPD-' . $ipdId)));
|
$ipdNo = trim((string) ($ipdRow['ipd_no'] ?? $ipdRow['ipd_code'] ?? ('IPD-' . $ipdId)));
|
||||||
|
|
||||||
|
$patientMobile = trim((string) ($patientRow['mphone1'] ?? $patientRow['p_mobile1'] ?? $ipdRow['P_mobile1'] ?? ''));
|
||||||
|
$patientAddress = trim((string) ($patientRow['p_add1'] ?? $patientRow['add1'] ?? $ipdRow['add1'] ?? ''));
|
||||||
|
$patientCity = trim((string) ($patientRow['p_city'] ?? $patientRow['city'] ?? $ipdRow['city'] ?? ''));
|
||||||
|
$patientState = trim((string) ($patientRow['p_state'] ?? $patientRow['state'] ?? $ipdRow['state'] ?? ''));
|
||||||
|
$patientPin = trim((string) ($patientRow['p_pin'] ?? $patientRow['pincode'] ?? ''));
|
||||||
|
$dischargeStatus = trim((string) ($ipdRow['discarge_patient_status'] ?? ''));
|
||||||
|
|
||||||
$source = [
|
$source = [
|
||||||
'record_id' => (string) $ipdId,
|
'record_id' => (string) $ipdId,
|
||||||
'bundle_identifier' => 'discharge-' . $ipdNo,
|
'bundle_identifier' => 'discharge-' . $ipdNo,
|
||||||
|
|
@ -9470,6 +9369,8 @@ class Ipd_discharge extends BaseController
|
||||||
'completed_at' => $dischargeIso,
|
'completed_at' => $dischargeIso,
|
||||||
'department' => trim((string) ($ipdRow['dept_id'] ?? '')),
|
'department' => trim((string) ($ipdRow['dept_id'] ?? '')),
|
||||||
'doctor_name' => trim((string) ($ipdRow['r_doc_name'] ?? '')),
|
'doctor_name' => trim((string) ($ipdRow['r_doc_name'] ?? '')),
|
||||||
|
'discharge_status' => $dischargeStatus,
|
||||||
|
'problem' => trim((string) ($ipdRow['problem'] ?? '')),
|
||||||
'organization' => [
|
'organization' => [
|
||||||
'id' => $hfrId,
|
'id' => $hfrId,
|
||||||
'name' => $hospitalName,
|
'name' => $hospitalName,
|
||||||
|
|
@ -9480,6 +9381,13 @@ class Ipd_discharge extends BaseController
|
||||||
'name' => $patientName,
|
'name' => $patientName,
|
||||||
'gender' => $this->normalizeFhirGender($genderRaw),
|
'gender' => $this->normalizeFhirGender($genderRaw),
|
||||||
'dob' => ! empty($patientRow['dob']) ? date('Y-m-d', strtotime((string) $patientRow['dob'])) : '',
|
'dob' => ! empty($patientRow['dob']) ? date('Y-m-d', strtotime((string) $patientRow['dob'])) : '',
|
||||||
|
'mobile' => $patientMobile,
|
||||||
|
'address' => [
|
||||||
|
'line' => $patientAddress,
|
||||||
|
'city' => $patientCity,
|
||||||
|
'state' => $patientState,
|
||||||
|
'pincode' => $patientPin,
|
||||||
|
],
|
||||||
'abha_id' => $abhaDigits,
|
'abha_id' => $abhaDigits,
|
||||||
'abha_address' => $abhaAddress,
|
'abha_address' => $abhaAddress,
|
||||||
],
|
],
|
||||||
|
|
@ -9489,6 +9397,7 @@ class Ipd_discharge extends BaseController
|
||||||
'class_code' => 'IMP',
|
'class_code' => 'IMP',
|
||||||
'start' => $admissionIso,
|
'start' => $admissionIso,
|
||||||
'end' => $dischargeIso,
|
'end' => $dischargeIso,
|
||||||
|
'discharge_disposition' => $dischargeStatus,
|
||||||
'location_display' => (string) ($locationInfo['display'] ?? ''),
|
'location_display' => (string) ($locationInfo['display'] ?? ''),
|
||||||
'ward' => (string) ($locationInfo['ward'] ?? ''),
|
'ward' => (string) ($locationInfo['ward'] ?? ''),
|
||||||
'room' => (string) ($locationInfo['room'] ?? ''),
|
'room' => (string) ($locationInfo['room'] ?? ''),
|
||||||
|
|
@ -9502,11 +9411,11 @@ class Ipd_discharge extends BaseController
|
||||||
'ui_surgeries' => $procedureRows,
|
'ui_surgeries' => $procedureRows,
|
||||||
'ui_final_diagnosis' => $conditionRows,
|
'ui_final_diagnosis' => $conditionRows,
|
||||||
'ui_summary_investigation' => $uiSummaryInvestigation,
|
'ui_summary_investigation' => $uiSummaryInvestigation,
|
||||||
'ui_course_treatment' => $courseRows,
|
'ui_course_treatment' => $courseRows,
|
||||||
|
'ui_course_treatment_narrative' => $courseRemark,
|
||||||
'ui_discharge_medicine' => $medicationRows,
|
'ui_discharge_medicine' => $medicationRows,
|
||||||
'allergies' => $allergyRows,
|
'allergies' => $allergyRows,
|
||||||
'documents' => [],
|
'documents' => $documentsList,
|
||||||
'skip_pdf' => true,
|
|
||||||
'template' => $this->resolveAbdmDischargeTemplate(),
|
'template' => $this->resolveAbdmDischargeTemplate(),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -287,7 +287,7 @@ class FhirDocumentBuilder
|
||||||
{
|
{
|
||||||
$out = [];
|
$out = [];
|
||||||
foreach ($data as $key => $value) {
|
foreach ($data as $key => $value) {
|
||||||
if ($value === null || $value === '') {
|
if ($value === null || $value === '' || $this->isPlaceholderValue($value)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (is_array($value)) {
|
if (is_array($value)) {
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,71 @@ abstract class AbstractModuleFhirGenerator
|
||||||
$patient['birthDate'] = $dob;
|
$patient['birthDate'] = $dob;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$mobile = trim((string) ($source['patient']['mobile'] ?? $source['patient']['phone'] ?? $source['patient']['p_mobile1'] ?? $source['mobile'] ?? ''));
|
||||||
|
if ($mobile !== '' && $this->isMeaningfulValue($mobile)) {
|
||||||
|
$digits = preg_replace('/\D/', '', $mobile);
|
||||||
|
if (is_string($digits) && strlen($digits) >= 10) {
|
||||||
|
$phoneVal = str_starts_with($mobile, '+') ? $mobile : ('+91' . substr($digits, -10));
|
||||||
|
$patient['telecom'] = [
|
||||||
|
[
|
||||||
|
'system' => 'phone',
|
||||||
|
'value' => $phoneVal,
|
||||||
|
'use' => 'mobile',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$addrRaw = $source['patient']['address'] ?? $source['address'] ?? null;
|
||||||
|
$line = '';
|
||||||
|
$city = '';
|
||||||
|
$district = '';
|
||||||
|
$state = '';
|
||||||
|
$postalCode = '';
|
||||||
|
|
||||||
|
if (is_array($addrRaw)) {
|
||||||
|
$line = trim((string) ($addrRaw['line'] ?? $addrRaw['text'] ?? $addrRaw['street'] ?? $addrRaw['add1'] ?? ''));
|
||||||
|
$city = trim((string) ($addrRaw['city'] ?? ''));
|
||||||
|
$district = trim((string) ($addrRaw['district'] ?? ''));
|
||||||
|
$state = trim((string) ($addrRaw['state'] ?? ''));
|
||||||
|
$postalCode = trim((string) ($addrRaw['postalCode'] ?? $addrRaw['pincode'] ?? $addrRaw['pin'] ?? ''));
|
||||||
|
} elseif (is_string($addrRaw) && $this->isMeaningfulValue($addrRaw)) {
|
||||||
|
$line = trim($addrRaw);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($city === '') {
|
||||||
|
$city = trim((string) ($source['patient']['city'] ?? $source['city'] ?? ''));
|
||||||
|
}
|
||||||
|
if ($state === '') {
|
||||||
|
$state = trim((string) ($source['patient']['state'] ?? $source['state'] ?? ''));
|
||||||
|
}
|
||||||
|
if ($postalCode === '') {
|
||||||
|
$postalCode = trim((string) ($source['patient']['postalCode'] ?? $source['patient']['pincode'] ?? $source['patient']['pin'] ?? $source['pincode'] ?? ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
$addressItem = [];
|
||||||
|
if ($line !== '' && $this->isMeaningfulValue($line)) {
|
||||||
|
$addressItem['line'] = [$line];
|
||||||
|
$addressItem['text'] = $line;
|
||||||
|
}
|
||||||
|
if ($city !== '' && $this->isMeaningfulValue($city)) {
|
||||||
|
$addressItem['city'] = $city;
|
||||||
|
}
|
||||||
|
if ($district !== '' && $this->isMeaningfulValue($district)) {
|
||||||
|
$addressItem['district'] = $district;
|
||||||
|
}
|
||||||
|
if ($state !== '' && $this->isMeaningfulValue($state)) {
|
||||||
|
$addressItem['state'] = $state;
|
||||||
|
}
|
||||||
|
if ($postalCode !== '' && $this->isMeaningfulValue($postalCode)) {
|
||||||
|
$addressItem['postalCode'] = $postalCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! empty($addressItem)) {
|
||||||
|
$addressItem['country'] = 'IND';
|
||||||
|
$patient['address'] = [$addressItem];
|
||||||
|
}
|
||||||
|
|
||||||
$abhaId = preg_replace('/\D/', '', (string) ($source['patient']['abha_id'] ?? ''));
|
$abhaId = preg_replace('/\D/', '', (string) ($source['patient']['abha_id'] ?? ''));
|
||||||
if (is_string($abhaId) && $abhaId !== '') {
|
if (is_string($abhaId) && $abhaId !== '') {
|
||||||
$patient['identifier'][] = [
|
$patient['identifier'][] = [
|
||||||
|
|
@ -96,6 +161,19 @@ abstract class AbstractModuleFhirGenerator
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$abhaAddress = trim((string) ($source['patient']['abha_address'] ?? $source['abha_address'] ?? ''));
|
||||||
|
if ($abhaAddress !== '' && str_contains($abhaAddress, '@')) {
|
||||||
|
$patient['identifier'][] = [
|
||||||
|
'type' => ['coding' => [[
|
||||||
|
'system' => 'http://terminology.hl7.org/CodeSystem/v2-0203',
|
||||||
|
'code' => 'MR',
|
||||||
|
'display' => 'Medical record number',
|
||||||
|
]], 'text' => 'ABHA Address'],
|
||||||
|
'system' => 'https://healthid.ndhm.gov.in',
|
||||||
|
'value' => $abhaAddress,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
$uhid = trim((string) ($source['patient']['uhid'] ?? $source['patient']['hospital_patient_id'] ?? $source['patient']['patient_code'] ?? ''));
|
$uhid = trim((string) ($source['patient']['uhid'] ?? $source['patient']['hospital_patient_id'] ?? $source['patient']['patient_code'] ?? ''));
|
||||||
if ($this->isMeaningfulValue($uhid)) {
|
if ($this->isMeaningfulValue($uhid)) {
|
||||||
$patient['identifier'][] = [
|
$patient['identifier'][] = [
|
||||||
|
|
@ -177,7 +255,10 @@ abstract class AbstractModuleFhirGenerator
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($encounterRef !== null) {
|
if ($encounterRef !== null) {
|
||||||
$composition['encounter'] = ['reference' => $encounterRef];
|
$composition['encounter'] = [
|
||||||
|
'reference' => $encounterRef,
|
||||||
|
'display' => 'Encounter',
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return $composition;
|
return $composition;
|
||||||
|
|
@ -231,6 +312,9 @@ abstract class AbstractModuleFhirGenerator
|
||||||
|
|
||||||
$patientName = trim((string) ($source['patient']['name'] ?? $source['patient_name'] ?? 'Patient'));
|
$patientName = trim((string) ($source['patient']['name'] ?? $source['patient_name'] ?? 'Patient'));
|
||||||
|
|
||||||
|
$statusInput = $enc['discharge_disposition'] ?? $enc['outcome'] ?? $source['discharge_status'] ?? $source['discarge_patient_status'] ?? 'home';
|
||||||
|
$disposition = $this->resolveDischargeDisposition($statusInput);
|
||||||
|
|
||||||
$encounterResource = [
|
$encounterResource = [
|
||||||
'resourceType' => 'Encounter',
|
'resourceType' => 'Encounter',
|
||||||
'id' => 'encounter-' . $id,
|
'id' => 'encounter-' . $id,
|
||||||
|
|
@ -239,7 +323,7 @@ abstract class AbstractModuleFhirGenerator
|
||||||
]],
|
]],
|
||||||
'text' => [
|
'text' => [
|
||||||
'status' => 'generated',
|
'status' => 'generated',
|
||||||
'div' => '<div xmlns="http://www.w3.org/1999/xhtml"><p><b>Encounter:</b> ' . $this->escapeXhtml($classDisplay) . ' ' . $this->escapeXhtml($id) . '</p></div>',
|
'div' => '<div xmlns="http://www.w3.org/1999/xhtml"><p><b>Encounter:</b> ' . $this->escapeXhtml($classDisplay) . ' ' . $this->escapeXhtml($id) . ' - ' . $this->escapeXhtml($disposition['text']) . '</p></div>',
|
||||||
],
|
],
|
||||||
'identifier' => $identifiers,
|
'identifier' => $identifiers,
|
||||||
'status' => 'finished',
|
'status' => 'finished',
|
||||||
|
|
@ -260,17 +344,74 @@ abstract class AbstractModuleFhirGenerator
|
||||||
'dischargeDisposition' => [
|
'dischargeDisposition' => [
|
||||||
'coding' => [[
|
'coding' => [[
|
||||||
'system' => 'http://terminology.hl7.org/CodeSystem/discharge-disposition',
|
'system' => 'http://terminology.hl7.org/CodeSystem/discharge-disposition',
|
||||||
'code' => 'home',
|
'code' => $disposition['code'],
|
||||||
'display' => 'Home',
|
'display' => $disposition['display'],
|
||||||
]],
|
]],
|
||||||
'text' => 'Discharged to Home Care',
|
'text' => $disposition['text'],
|
||||||
]
|
],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
return $encounterResource;
|
return $encounterResource;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $status
|
||||||
|
* @return array{code:string,display:string,text:string}
|
||||||
|
*/
|
||||||
|
protected function resolveDischargeDisposition(mixed $status): array
|
||||||
|
{
|
||||||
|
$statusStr = strtolower(trim((string) $status));
|
||||||
|
if ($statusStr === '1' || str_contains($statusStr, 'improved') || str_contains($statusStr, 'recovered')) {
|
||||||
|
return [
|
||||||
|
'code' => 'home',
|
||||||
|
'display' => 'Home',
|
||||||
|
'text' => 'Improved / Recovered - Discharged to Home Care',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if ($statusStr === '2' || str_contains($statusStr, 'lama') || str_contains($statusStr, 'advice')) {
|
||||||
|
return [
|
||||||
|
'code' => 'aadvice',
|
||||||
|
'display' => 'Left against medical advice',
|
||||||
|
'text' => 'Left Against Medical Advice (LAMA)',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if ($statusStr === '3' || str_contains($statusStr, 'refer') || str_contains($statusStr, 'higher')) {
|
||||||
|
return [
|
||||||
|
'code' => 'other-hcf',
|
||||||
|
'display' => 'Other healthcare facility',
|
||||||
|
'text' => 'Referred to Higher Centre / Other Healthcare Facility',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if ($statusStr === '4' || str_contains($statusStr, 'satisfactory')) {
|
||||||
|
return [
|
||||||
|
'code' => 'home',
|
||||||
|
'display' => 'Home',
|
||||||
|
'text' => 'Satisfactory - Discharged to Home Care',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if ($statusStr === '5' || str_contains($statusStr, 'expir') || str_contains($statusStr, 'dead') || str_contains($statusStr, 'deceased')) {
|
||||||
|
return [
|
||||||
|
'code' => 'exp',
|
||||||
|
'display' => 'Expired',
|
||||||
|
'text' => 'Expired / Deceased',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if ($statusStr === '7' || str_contains($statusStr, 'request')) {
|
||||||
|
return [
|
||||||
|
'code' => 'home',
|
||||||
|
'display' => 'Home',
|
||||||
|
'text' => 'Discharge on Request - Discharged to Home Care',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'code' => 'home',
|
||||||
|
'display' => 'Home',
|
||||||
|
'text' => 'Discharged to Home Care',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/** @param array<string,mixed> $source */
|
/** @param array<string,mixed> $source */
|
||||||
protected function buildPractitioner(array $source): ?array
|
protected function buildPractitioner(array $source): ?array
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -148,6 +148,31 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is_array($encounter)) {
|
if (is_array($encounter)) {
|
||||||
|
$encounterDiagnoses = [];
|
||||||
|
foreach ($diagnosesList as $idx => $cond) {
|
||||||
|
$text = trim((string) ($cond['text'] ?? $cond['name'] ?? ''));
|
||||||
|
if ($text === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$conditionId = 'discharge-diagnosis-' . $recordId . '-' . $idx;
|
||||||
|
$encounterDiagnoses[] = [
|
||||||
|
'condition' => [
|
||||||
|
'reference' => 'urn:uuid:' . $conditionId,
|
||||||
|
'display' => $text,
|
||||||
|
],
|
||||||
|
'use' => [
|
||||||
|
'coding' => [[
|
||||||
|
'system' => 'http://terminology.hl7.org/CodeSystem/diagnosis-role',
|
||||||
|
'code' => 'DD',
|
||||||
|
'display' => 'Discharge diagnosis',
|
||||||
|
]],
|
||||||
|
],
|
||||||
|
'rank' => count($encounterDiagnoses) + 1,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if ($encounterDiagnoses !== []) {
|
||||||
|
$encounter['diagnosis'] = $encounterDiagnoses;
|
||||||
|
}
|
||||||
$builder->addEncounter($encounter);
|
$builder->addEncounter($encounter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -407,8 +432,30 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ── Care Plans / Discharge Advice ──────────────────────────────────────
|
// ── Care Plans / Discharge Advice & Hospital Course ────────────────────
|
||||||
foreach ((array) ($source['ui_discharge_summary'] ?? $source['care_plans'] ?? []) as $idx => $cp) {
|
$carePlansList = (array) ($source['ui_discharge_summary'] ?? $source['care_plans'] ?? []);
|
||||||
|
|
||||||
|
// Include hospital course / course of treatment if provided
|
||||||
|
$courseItems = (array) ($source['ui_course_treatment'] ?? $source['course_treatment'] ?? []);
|
||||||
|
$courseNarrative = trim((string) ($source['ui_course_treatment_narrative'] ?? $source['course_narrative'] ?? ''));
|
||||||
|
$courseParts = [];
|
||||||
|
foreach ($courseItems as $cItem) {
|
||||||
|
$cText = is_array($cItem) ? trim((string) ($cItem['text'] ?? $cItem['name'] ?? $cItem['comp_report'] ?? '')) : trim((string) $cItem);
|
||||||
|
if ($cText !== '' && $this->isMeaningfulValue($cText)) {
|
||||||
|
$courseParts[] = $cText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($courseNarrative !== '' && $this->isMeaningfulValue($courseNarrative)) {
|
||||||
|
$courseParts[] = $courseNarrative;
|
||||||
|
}
|
||||||
|
if (! empty($courseParts)) {
|
||||||
|
$carePlansList[] = [
|
||||||
|
'title' => 'Course of Treatment in Hospital',
|
||||||
|
'description' => implode("\n", array_unique($courseParts)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($carePlansList as $idx => $cp) {
|
||||||
$title = $this->cleanPlainText((string) ($cp['title'] ?? 'Discharge Advice'));
|
$title = $this->cleanPlainText((string) ($cp['title'] ?? 'Discharge Advice'));
|
||||||
$description = $this->cleanPlainText((string) ($cp['description'] ?? ''));
|
$description = $this->cleanPlainText((string) ($cp['description'] ?? ''));
|
||||||
if (! $this->isMeaningfulValue($description)) {
|
if (! $this->isMeaningfulValue($description)) {
|
||||||
|
|
@ -544,10 +591,9 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
|
||||||
$sectionDefinitions[] = ['title' => 'Investigations', 'code' => '721981007', 'display' => 'Diagnostic studies report', 'prefix' => 'discharge-investigation-', 'items' => $investigations, 'narrative' => $invNarrative];
|
$sectionDefinitions[] = ['title' => 'Investigations', 'code' => '721981007', 'display' => 'Diagnostic studies report', 'prefix' => 'discharge-investigation-', 'items' => $investigations, 'narrative' => $invNarrative];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Care plans / Discharge advice
|
// Care plans / Discharge advice & hospital course
|
||||||
$carePlans = (array) ($source['ui_discharge_summary'] ?? $source['care_plans'] ?? []);
|
if (! empty($carePlansList)) {
|
||||||
if (! empty($carePlans)) {
|
$sectionDefinitions[] = ['title' => 'Care Plan', 'code' => '734163000', 'display' => 'Care plan', 'prefix' => 'discharge-careplan-', 'items' => $carePlansList];
|
||||||
$sectionDefinitions[] = ['title' => 'Care Plan', 'code' => '734163000', 'display' => 'Care plan', 'prefix' => 'discharge-careplan-', 'items' => $carePlans];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Document Reference
|
// Document Reference
|
||||||
|
|
@ -865,13 +911,13 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
|
||||||
{
|
{
|
||||||
$hospitalName = trim((string) ($source['organization']['name'] ?? 'Hospital'));
|
$hospitalName = trim((string) ($source['organization']['name'] ?? 'Hospital'));
|
||||||
$patientName = trim((string) ($source['patient']['name'] ?? 'Patient'));
|
$patientName = trim((string) ($source['patient']['name'] ?? 'Patient'));
|
||||||
$patientId = trim((string) ($source['patient']['id'] ?? 'N/A'));
|
$patientId = trim((string) ($source['patient']['id'] ?? ''));
|
||||||
$gender = ucfirst(trim((string) ($source['patient']['gender'] ?? 'N/A')));
|
$gender = ucfirst(trim((string) ($source['patient']['gender'] ?? '')));
|
||||||
$dob = trim((string) ($source['patient']['dob'] ?? 'N/A'));
|
$dob = trim((string) ($source['patient']['dob'] ?? ''));
|
||||||
$abhaId = trim((string) ($source['patient']['abha_id'] ?? 'N/A'));
|
$abhaId = trim((string) ($source['patient']['abha_id'] ?? ''));
|
||||||
$recordId = trim((string) ($source['record_id'] ?? 'N/A'));
|
$recordId = trim((string) ($source['record_id'] ?? ''));
|
||||||
|
|
||||||
$admDate = ! empty($source['encounter']['start']) ? date('d-m-Y', strtotime((string) $source['encounter']['start'])) : 'N/A';
|
$admDate = ! empty($source['encounter']['start']) ? date('d-m-Y', strtotime((string) $source['encounter']['start'])) : '';
|
||||||
$disDate = ! empty($source['encounter']['end']) ? date('d-m-Y', strtotime((string) $source['encounter']['end'])) : date('d-m-Y');
|
$disDate = ! empty($source['encounter']['end']) ? date('d-m-Y', strtotime((string) $source['encounter']['end'])) : date('d-m-Y');
|
||||||
$doctorName = trim((string) ($source['doctor_name'] ?? $source['doctor']['name'] ?? 'Attending Physician'));
|
$doctorName = trim((string) ($source['doctor_name'] ?? $source['doctor']['name'] ?? 'Attending Physician'));
|
||||||
|
|
||||||
|
|
@ -898,7 +944,7 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
|
||||||
'{{PATIENT_NAME}}' => htmlspecialchars($patientName),
|
'{{PATIENT_NAME}}' => htmlspecialchars($patientName),
|
||||||
'{{UHID}}' => htmlspecialchars($patientId),
|
'{{UHID}}' => htmlspecialchars($patientId),
|
||||||
'{{IPD_CODE}}' => htmlspecialchars($recordId),
|
'{{IPD_CODE}}' => htmlspecialchars($recordId),
|
||||||
'{{AGE_GENDER}}' => htmlspecialchars($gender . ($dob !== 'N/A' ? ' / ' . $dob : '')),
|
'{{AGE_GENDER}}' => htmlspecialchars(trim($gender . ($dob !== '' ? ' / ' . $dob : ''))),
|
||||||
'{{ADMIT_DATE}}' => htmlspecialchars($admDate),
|
'{{ADMIT_DATE}}' => htmlspecialchars($admDate),
|
||||||
'{{DISCHARGE_DATE}}' => htmlspecialchars($disDate),
|
'{{DISCHARGE_DATE}}' => htmlspecialchars($disDate),
|
||||||
'{{DOCTOR_NAMES}}' => htmlspecialchars($doctorName),
|
'{{DOCTOR_NAMES}}' => htmlspecialchars($doctorName),
|
||||||
|
|
@ -1229,11 +1275,64 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
|
||||||
string $hospitalName,
|
string $hospitalName,
|
||||||
array $sections
|
array $sections
|
||||||
): string {
|
): string {
|
||||||
$ipdNo = (string) ($source['encounter']['ipd_no'] ?? $source['record_id'] ?? '');
|
$ipdNo = trim((string) ($source['encounter']['ipd_no'] ?? $source['record_id'] ?? ''));
|
||||||
$admitDate = (string) ($source['encounter']['start'] ?? $source['admission_date'] ?? '');
|
$admitRaw = trim((string) ($source['encounter']['start'] ?? $source['admission_date'] ?? ''));
|
||||||
$dischargeDate = (string) ($source['encounter']['end'] ?? $source['discharge_date'] ?? $source['completed_at'] ?? '');
|
$dischargeRaw = trim((string) ($source['encounter']['end'] ?? $source['discharge_date'] ?? $source['completed_at'] ?? ''));
|
||||||
$locDisplay = (string) ($source['encounter']['location_display'] ?? '');
|
|
||||||
$abhaId = (string) ($source['patient']['abha_id'] ?? '');
|
$formatDate = static function (string $raw): string {
|
||||||
|
if ($raw === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
$ts = strtotime($raw);
|
||||||
|
if ($ts === false) {
|
||||||
|
return $raw;
|
||||||
|
}
|
||||||
|
return (strlen($raw) > 10 && str_contains($raw, 'T')) ? date('d-M-Y h:i A', $ts) : date('d-M-Y', $ts);
|
||||||
|
};
|
||||||
|
|
||||||
|
$admitDisplay = $formatDate($admitRaw);
|
||||||
|
if ($admitDisplay === '') {
|
||||||
|
$admitDisplay = date('d-M-Y');
|
||||||
|
}
|
||||||
|
$dischargeDisplay = $formatDate($dischargeRaw);
|
||||||
|
if ($dischargeDisplay === '') {
|
||||||
|
$dischargeDisplay = date('d-M-Y');
|
||||||
|
}
|
||||||
|
|
||||||
|
$locDisplay = trim((string) ($source['encounter']['location_display'] ?? $source['encounter']['ward'] ?? ''));
|
||||||
|
$abhaId = trim((string) ($source['patient']['abha_id'] ?? ''));
|
||||||
|
$patientUhid = trim((string) ($source['patient']['uhid'] ?? $source['patient']['hospital_patient_id'] ?? ''));
|
||||||
|
$patientMobile = trim((string) ($source['patient']['mobile'] ?? $source['patient']['phone'] ?? $source['mobile'] ?? ''));
|
||||||
|
|
||||||
|
$identifierDisplay = '';
|
||||||
|
if ($abhaId !== '' && $this->isMeaningfulValue($abhaId)) {
|
||||||
|
$digits = preg_replace('/\D/', '', $abhaId);
|
||||||
|
$identifierDisplay = (strlen($digits) === 14)
|
||||||
|
? (substr($digits, 0, 2) . '-' . substr($digits, 2, 4) . '-' . substr($digits, 6, 4) . '-' . substr($digits, 10, 4))
|
||||||
|
: $abhaId;
|
||||||
|
} elseif ($patientUhid !== '') {
|
||||||
|
$identifierDisplay = 'UHID: ' . $patientUhid;
|
||||||
|
} elseif ($patientId !== '' && $patientId !== '0') {
|
||||||
|
$identifierDisplay = 'ID: ' . $patientId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Outcome / Discharge status
|
||||||
|
$statusInput = $source['encounter']['discharge_disposition'] ?? $source['discharge_status'] ?? $source['discarge_patient_status'] ?? 'home';
|
||||||
|
$disposition = $this->resolveDischargeDisposition($statusInput);
|
||||||
|
$outcomeText = $disposition['text'];
|
||||||
|
|
||||||
|
// Diagnosis summary
|
||||||
|
$diagList = [];
|
||||||
|
foreach ((array) ($source['ui_final_diagnosis'] ?? $source['diagnoses'] ?? $source['conditions'] ?? []) as $d) {
|
||||||
|
$t = is_array($d) ? trim((string) ($d['text'] ?? $d['name'] ?? '')) : trim((string) $d);
|
||||||
|
if ($t !== '' && $this->isMeaningfulValue($t)) {
|
||||||
|
$diagList[] = $t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (empty($diagList) && ! empty($source['problem']) && $this->isMeaningfulValue((string) $source['problem'])) {
|
||||||
|
$diagList[] = trim((string) $source['problem']);
|
||||||
|
}
|
||||||
|
$diagnosisSummary = ! empty($diagList) ? implode(', ', array_unique($diagList)) : '';
|
||||||
|
|
||||||
$html = '<div xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-IN" lang="en-IN">';
|
$html = '<div xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-IN" lang="en-IN">';
|
||||||
$html .= '<div style="border-bottom:2px solid #0284c7;padding-bottom:6px;margin-bottom:12px;">';
|
$html .= '<div style="border-bottom:2px solid #0284c7;padding-bottom:6px;margin-bottom:12px;">';
|
||||||
|
|
@ -1242,11 +1341,22 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
|
||||||
$html .= '</div>';
|
$html .= '</div>';
|
||||||
|
|
||||||
$html .= '<table style="width:100%;border-collapse:collapse;margin-bottom:14px;background:#f8fafc;font-size:10pt;">';
|
$html .= '<table style="width:100%;border-collapse:collapse;margin-bottom:14px;background:#f8fafc;font-size:10pt;">';
|
||||||
$html .= '<tr><td style="border:1px solid #cbd5e1;padding:6px;"><strong>Patient:</strong> ' . htmlspecialchars($patientName) . '</td><td style="border:1px solid #cbd5e1;padding:6px;"><strong>IPD No:</strong> ' . htmlspecialchars($ipdNo) . '</td></tr>';
|
$html .= '<tr><td style="border:1px solid #cbd5e1;padding:6px;"><strong>Patient:</strong> ' . htmlspecialchars($patientName) . '</td><td style="border:1px solid #cbd5e1;padding:6px;"><strong>IPD No:</strong> ' . htmlspecialchars($ipdNo !== '' ? $ipdNo : ('IPD-' . $patientId)) . '</td></tr>';
|
||||||
$html .= '<tr><td style="border:1px solid #cbd5e1;padding:6px;"><strong>Gender / DOB:</strong> ' . htmlspecialchars(ucfirst($gender)) . ($dob !== '' ? (' / ' . htmlspecialchars($dob)) : '') . '</td><td style="border:1px solid #cbd5e1;padding:6px;"><strong>ABHA Number:</strong> ' . htmlspecialchars($abhaId !== '' ? $abhaId : 'N/A') . '</td></tr>';
|
|
||||||
$html .= '<tr><td style="border:1px solid #cbd5e1;padding:6px;"><strong>Admission Date:</strong> ' . htmlspecialchars($admitDate) . '</td><td style="border:1px solid #cbd5e1;padding:6px;"><strong>Discharge Date:</strong> ' . htmlspecialchars($dischargeDate) . '</td></tr>';
|
$genderDob = htmlspecialchars(ucfirst($gender)) . ($dob !== '' ? (' / ' . htmlspecialchars($dob)) : '');
|
||||||
if ($locDisplay !== '' || $drName !== '') {
|
$idCell = $identifierDisplay !== '' ? ('<strong>ABHA / ID:</strong> ' . htmlspecialchars($identifierDisplay)) : ('<strong>Doctor:</strong> ' . htmlspecialchars($drName));
|
||||||
$html .= '<tr><td style="border:1px solid #cbd5e1;padding:6px;"><strong>Location / Bed:</strong> ' . htmlspecialchars($locDisplay !== '' ? $locDisplay : 'General Ward') . '</td><td style="border:1px solid #cbd5e1;padding:6px;"><strong>Doctor:</strong> ' . htmlspecialchars($drName) . '</td></tr>';
|
$html .= '<tr><td style="border:1px solid #cbd5e1;padding:6px;"><strong>Gender / DOB:</strong> ' . $genderDob . '</td><td style="border:1px solid #cbd5e1;padding:6px;">' . $idCell . '</td></tr>';
|
||||||
|
|
||||||
|
$html .= '<tr><td style="border:1px solid #cbd5e1;padding:6px;"><strong>Admission Date:</strong> ' . htmlspecialchars($admitDisplay) . '</td><td style="border:1px solid #cbd5e1;padding:6px;"><strong>Discharge Date:</strong> ' . htmlspecialchars($dischargeDisplay) . '</td></tr>';
|
||||||
|
|
||||||
|
if ($diagnosisSummary !== '') {
|
||||||
|
$html .= '<tr><td style="border:1px solid #cbd5e1;padding:6px;" colspan="2"><strong>Final Diagnosis:</strong> ' . htmlspecialchars($diagnosisSummary) . '</td></tr>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$html .= '<tr><td style="border:1px solid #cbd5e1;padding:6px;"><strong>Outcome / Status:</strong> <span style="color:#0369a1;font-weight:bold;">' . htmlspecialchars($outcomeText) . '</span></td><td style="border:1px solid #cbd5e1;padding:6px;"><strong>Doctor:</strong> ' . htmlspecialchars($drName) . ($locDisplay !== '' ? (' | <strong>Location:</strong> ' . htmlspecialchars($locDisplay)) : '') . '</td></tr>';
|
||||||
|
|
||||||
|
if ($patientMobile !== '') {
|
||||||
|
$html .= '<tr><td style="border:1px solid #cbd5e1;padding:6px;" colspan="2"><strong>Contact:</strong> ' . htmlspecialchars($patientMobile) . '</td></tr>';
|
||||||
}
|
}
|
||||||
$html .= '</table>';
|
$html .= '</table>';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,21 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td><?= (int) $i + 1 ?></td>
|
<td><?= (int) $i + 1 ?></td>
|
||||||
<td><?= esc((string) ($row['template_name'] ?? '')) ?></td>
|
<td><?= esc((string) ($row['template_name'] ?? '')) ?></td>
|
||||||
<td><?= (int) ($row['is_default'] ?? 0) ? '<span class="badge bg-success">Yes</span>' : '' ?></td>
|
<td>
|
||||||
|
<?php if ((int) ($row['is_default'] ?? 0)): ?>
|
||||||
|
<span class="badge bg-success"><i class="fa fa-check me-1"></i>Yes</span>
|
||||||
|
<?php else: ?>
|
||||||
|
<button type="button" class="btn btn-outline-success btn-sm py-0 px-2 btn_set_default" data-id="<?= (int) ($row['id'] ?? 0) ?>" title="Set as default template">Set Default</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
<td><?= (int) ($row['is_audit_only'] ?? 0) ? '<span class="badge bg-warning text-dark">Audit</span>' : '' ?></td>
|
<td><?= (int) ($row['is_audit_only'] ?? 0) ? '<span class="badge bg-warning text-dark">Audit</span>' : '' ?></td>
|
||||||
<td><?= (int) ($row['is_abdm'] ?? 0) ? '<span class="badge bg-info text-dark">Online / ABDM</span>' : '' ?></td>
|
<td>
|
||||||
|
<?php if ((int) ($row['is_abdm'] ?? 0)): ?>
|
||||||
|
<span class="badge bg-info text-dark"><i class="fa fa-check me-1"></i>Online / ABDM</span>
|
||||||
|
<?php else: ?>
|
||||||
|
<button type="button" class="btn btn-outline-info btn-sm py-0 px-2 btn_set_abdm" data-id="<?= (int) ($row['id'] ?? 0) ?>" title="Set as Online / ABDM template">Set ABDM</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a class="btn btn-primary btn-sm"
|
<a class="btn btn-primary btn-sm"
|
||||||
href="javascript:load_form('<?= base_url('Ipd_discharge/print_template_builder') ?>?mode=edit&edit=<?= (int) ($row['id'] ?? 0) ?>','IPD Discharge Template Edit');">Edit</a>
|
href="javascript:load_form('<?= base_url('Ipd_discharge/print_template_builder') ?>?mode=edit&edit=<?= (int) ($row['id'] ?? 0) ?>','IPD Discharge Template Edit');">Edit</a>
|
||||||
|
|
@ -150,5 +162,51 @@
|
||||||
}).catch(function () { showMsg('Request failed.', false); });
|
}).catch(function () { showMsg('Request failed.', false); });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.btn_set_abdm').forEach(function (btn) {
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
var id = btn.getAttribute('data-id');
|
||||||
|
var csrf = getCsrf();
|
||||||
|
var fd = new FormData();
|
||||||
|
fd.append(csrf.name, csrf.value);
|
||||||
|
fd.append('id', id);
|
||||||
|
|
||||||
|
fetch('<?= base_url('Ipd_discharge/discharge_template_set_abdm_ajax') ?>', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'X-Requested-With': 'XMLHttpRequest'},
|
||||||
|
body: fd
|
||||||
|
}).then(function (r) { return r.json(); }).then(function (data) {
|
||||||
|
updateCsrf(data);
|
||||||
|
if (data.update == 1) {
|
||||||
|
load_form('<?= base_url('Ipd_discharge/print_template_builder') ?>?mode=list', 'IPD Discharge Templates');
|
||||||
|
} else {
|
||||||
|
showMsg(data.error_text || 'Unable to update Online / ABDM template.', false);
|
||||||
|
}
|
||||||
|
}).catch(function () { showMsg('Request failed.', false); });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.btn_set_default').forEach(function (btn) {
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
var id = btn.getAttribute('data-id');
|
||||||
|
var csrf = getCsrf();
|
||||||
|
var fd = new FormData();
|
||||||
|
fd.append(csrf.name, csrf.value);
|
||||||
|
fd.append('id', id);
|
||||||
|
|
||||||
|
fetch('<?= base_url('Ipd_discharge/discharge_template_set_default_ajax') ?>', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'X-Requested-With': 'XMLHttpRequest'},
|
||||||
|
body: fd
|
||||||
|
}).then(function (r) { return r.json(); }).then(function (data) {
|
||||||
|
updateCsrf(data);
|
||||||
|
if (data.update == 1) {
|
||||||
|
load_form('<?= base_url('Ipd_discharge/print_template_builder') ?>?mode=list', 'IPD Discharge Templates');
|
||||||
|
} else {
|
||||||
|
showMsg(data.error_text || 'Unable to update default template.', false);
|
||||||
|
}
|
||||||
|
}).catch(function () { showMsg('Request failed.', false); });
|
||||||
|
});
|
||||||
|
});
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -800,4 +800,128 @@ final class FhirGeneratorsTest extends CIUnitTestCase
|
||||||
$this->assertContains('Medications', $sectionTitles);
|
$this->assertContains('Medications', $sectionTitles);
|
||||||
$this->assertContains('Care Plan', $sectionTitles);
|
$this->assertContains('Care Plan', $sectionTitles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testDischargeMandatoryFieldsPatientEncounterDiagnosisAndOutcome(): void
|
||||||
|
{
|
||||||
|
$src = [
|
||||||
|
'record_id' => 9901,
|
||||||
|
'bundle_identifier' => 'discharge-A26090009901',
|
||||||
|
'session_id' => 9901,
|
||||||
|
'visit_date' => '2026-09-04',
|
||||||
|
'completed_at' => '2026-09-04T10:00:00+05:30',
|
||||||
|
'discharge_status' => 'LAMA',
|
||||||
|
'patient' => [
|
||||||
|
'id' => 1234,
|
||||||
|
'uhid' => 'P26091001234',
|
||||||
|
'name' => 'ANITA SHARMA',
|
||||||
|
'gender' => 'female',
|
||||||
|
'dob' => '1985-07-20',
|
||||||
|
'mobile' => '9876543210',
|
||||||
|
'address' => '123 Main Street, Civil Lines',
|
||||||
|
'city' => 'Haridwar',
|
||||||
|
'district' => 'Haridwar',
|
||||||
|
'state' => 'Uttarakhand',
|
||||||
|
'pin' => '249401',
|
||||||
|
'abha_id' => '91407564383099',
|
||||||
|
'abha_address' => 'anitasharma@sbx',
|
||||||
|
],
|
||||||
|
'encounter' => [
|
||||||
|
'id' => 'A26090009901',
|
||||||
|
'class_code' => 'IMP',
|
||||||
|
'start' => '2026-09-01T09:00:00+05:30',
|
||||||
|
'end' => '2026-09-04T10:00:00+05:30',
|
||||||
|
'discharge_disposition' => 'LAMA',
|
||||||
|
'ward' => 'ICU',
|
||||||
|
'bed' => 'ICU-02',
|
||||||
|
],
|
||||||
|
'doctor' => ['id' => 10, 'name' => 'Dr. R K Sundriyal'],
|
||||||
|
'organization' => ['id' => 'IN0510000000', 'name' => 'DevSoft Hospital'],
|
||||||
|
'chief_complaints' => [['text' => 'High grade fever with chills']],
|
||||||
|
'diagnoses' => [['text' => 'Enteric Fever / Typhoid']],
|
||||||
|
'procedures' => [['text' => 'Intravenous cannulation and fluid hydration']],
|
||||||
|
'medications' => [['name' => 'INJ CEFTRIAXONE 1GM', 'dosage' => 'IV | BD | for 5 Days']],
|
||||||
|
'ui_course_treatment' => [
|
||||||
|
'Patient admitted with high grade fever. Started on IV antibiotics and fluids. Vitals stabilized on Day 2. Patient requested LAMA on Day 4.',
|
||||||
|
],
|
||||||
|
'care_plans' => [
|
||||||
|
['title' => 'Follow Up Advice', 'description' => 'Consult local physician after 3 days.'],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$output = $this->factory->discharge()->generate($src);
|
||||||
|
$bundle = $output['fhir_bundle'];
|
||||||
|
$composition = $bundle['entry'][0]['resource'];
|
||||||
|
|
||||||
|
// 1. Patient Telecom and Address verification
|
||||||
|
$patients = array_values(array_filter($bundle['entry'], static fn (array $e): bool => ($e['resource']['resourceType'] ?? '') === 'Patient'));
|
||||||
|
$this->assertCount(1, $patients);
|
||||||
|
$patient = $patients[0]['resource'];
|
||||||
|
|
||||||
|
$this->assertSame('+919876543210', $patient['telecom'][0]['value'] ?? null);
|
||||||
|
$this->assertSame('phone', $patient['telecom'][0]['system'] ?? null);
|
||||||
|
$this->assertSame('mobile', $patient['telecom'][0]['use'] ?? null);
|
||||||
|
|
||||||
|
$this->assertSame('123 Main Street, Civil Lines', $patient['address'][0]['line'][0] ?? null);
|
||||||
|
$this->assertSame('Haridwar', $patient['address'][0]['city'] ?? null);
|
||||||
|
$this->assertSame('Uttarakhand', $patient['address'][0]['state'] ?? null);
|
||||||
|
$this->assertSame('249401', $patient['address'][0]['postalCode'] ?? null);
|
||||||
|
$this->assertSame('IND', $patient['address'][0]['country'] ?? null);
|
||||||
|
|
||||||
|
// ABHA address identifier
|
||||||
|
$patientIdSystems = array_column($patient['identifier'], 'system');
|
||||||
|
$this->assertContains('https://healthid.ndhm.gov.in', $patientIdSystems);
|
||||||
|
|
||||||
|
// 2. Encounter: Start/End period, hospitalization and discharge disposition
|
||||||
|
$encounters = array_values(array_filter($bundle['entry'], static fn (array $e): bool => ($e['resource']['resourceType'] ?? '') === 'Encounter'));
|
||||||
|
$this->assertCount(1, $encounters);
|
||||||
|
$encounter = $encounters[0]['resource'];
|
||||||
|
|
||||||
|
$this->assertSame('2026-09-01T09:00:00+05:30', $encounter['period']['start'] ?? null);
|
||||||
|
$this->assertSame('2026-09-04T10:00:00+05:30', $encounter['period']['end'] ?? null);
|
||||||
|
$this->assertSame('aadvice', $encounter['hospitalization']['dischargeDisposition']['coding'][0]['code'] ?? null);
|
||||||
|
$this->assertSame('Left against medical advice', $encounter['hospitalization']['dischargeDisposition']['coding'][0]['display'] ?? null);
|
||||||
|
|
||||||
|
// 3. Encounter.diagnosis linking to Final Diagnosis Condition
|
||||||
|
$this->assertNotEmpty($encounter['diagnosis'] ?? []);
|
||||||
|
$this->assertSame('DD', $encounter['diagnosis'][0]['use']['coding'][0]['code'] ?? null);
|
||||||
|
$diagRef = $encounter['diagnosis'][0]['condition']['reference'] ?? '';
|
||||||
|
$this->assertStringStartsWith('urn:uuid:', $diagRef);
|
||||||
|
|
||||||
|
// Verify that diagRef points to the final diagnosis Condition
|
||||||
|
$conditionUrls = array_column(array_filter($bundle['entry'], static fn (array $e): bool => ($e['resource']['resourceType'] ?? '') === 'Condition'), 'fullUrl');
|
||||||
|
$this->assertContains($diagRef, $conditionUrls);
|
||||||
|
|
||||||
|
// 4. CarePlan for Course of Treatment
|
||||||
|
$carePlans = array_values(array_filter($bundle['entry'], static fn (array $e): bool => ($e['resource']['resourceType'] ?? '') === 'CarePlan'));
|
||||||
|
$carePlanTitles = array_column(array_column($carePlans, 'resource'), 'title');
|
||||||
|
$this->assertContains('Course of Treatment in Hospital', $carePlanTitles);
|
||||||
|
|
||||||
|
// 5. Composition narrative XHTML checks
|
||||||
|
$narrative = $composition['text']['div'] ?? '';
|
||||||
|
$this->assertStringContainsString('ANITA SHARMA', $narrative);
|
||||||
|
$this->assertStringContainsString('9876543210', $narrative);
|
||||||
|
$this->assertStringContainsString('Enteric Fever / Typhoid', $narrative);
|
||||||
|
$this->assertStringContainsString('Left Against Medical Advice', $narrative);
|
||||||
|
$this->assertStringContainsString('01-Sep-2026', $narrative);
|
||||||
|
$this->assertStringContainsString('04-Sep-2026', $narrative);
|
||||||
|
|
||||||
|
// Assert NO standalone placeholder "NA" or "N/A"
|
||||||
|
$this->assertDoesNotMatchRegularExpression('/>\s*(NA|N\/A|null)\s*</i', $narrative);
|
||||||
|
|
||||||
|
// Also test other disposition mappings
|
||||||
|
$srcCured = $src;
|
||||||
|
$srcCured['discharge_status'] = 'Cured / Improved';
|
||||||
|
unset($srcCured['encounter']['discharge_disposition']);
|
||||||
|
$outCured = $this->factory->discharge()->generate($srcCured);
|
||||||
|
$encCured = array_values(array_filter($outCured['fhir_bundle']['entry'], static fn (array $e): bool => ($e['resource']['resourceType'] ?? '') === 'Encounter'))[0]['resource'];
|
||||||
|
$this->assertSame('home', $encCured['hospitalization']['dischargeDisposition']['coding'][0]['code']);
|
||||||
|
|
||||||
|
$srcExp = $src;
|
||||||
|
$srcExp['discharge_status'] = 'Expired';
|
||||||
|
unset($srcExp['encounter']['discharge_disposition']);
|
||||||
|
$outExp = $this->factory->discharge()->generate($srcExp);
|
||||||
|
$encExp = array_values(array_filter($outExp['fhir_bundle']['entry'], static fn (array $e): bool => ($e['resource']['resourceType'] ?? '') === 'Encounter'))[0]['resource'];
|
||||||
|
$this->assertSame('exp', $encExp['hospitalization']['dischargeDisposition']['coding'][0]['code']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user