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->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_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->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');
|
||||
|
|
|
|||
|
|
@ -3064,6 +3064,28 @@ class AbdmGateway extends BaseController
|
|||
$documents = $this->buildIpdPdfDocuments($ipdId, $dischargeRaw);
|
||||
$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()) : '';
|
||||
|
||||
$source = [
|
||||
|
|
@ -3075,6 +3097,8 @@ class AbdmGateway extends BaseController
|
|||
'completed_at' => $this->toIsoDateTimeOrNow($dischargeRaw),
|
||||
'department' => trim((string) ($ipdRow['dept_id'] ?? '')),
|
||||
'doctor_name' => $doctorName,
|
||||
'discharge_status' => $dischargeStatus,
|
||||
'problem' => trim((string) ($ipdRow['problem'] ?? '')),
|
||||
'doctor' => [
|
||||
'id' => $doctorId > 0 ? (string) $doctorId : '',
|
||||
'name' => $doctorName,
|
||||
|
|
@ -3091,6 +3115,13 @@ class AbdmGateway extends BaseController
|
|||
($g = trim((string) ($patientRow['gender'] ?? ''))) !== '' ? $g : trim((string) ($patientRow['xgender'] ?? ''))
|
||||
),
|
||||
'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_address' => $abhaAddress,
|
||||
],
|
||||
|
|
@ -3100,6 +3131,7 @@ class AbdmGateway extends BaseController
|
|||
'class_code' => 'IMP',
|
||||
'start' => $this->toIsoDateTimeOrNow($admissionRaw),
|
||||
'end' => $this->toIsoDateTimeOrNow($dischargeRaw),
|
||||
'discharge_disposition' => $dischargeStatus,
|
||||
'location_display' => (string) ($locationInfo['display'] ?? ''),
|
||||
'ward' => (string) ($locationInfo['ward'] ?? ''),
|
||||
'room' => (string) ($locationInfo['room'] ?? ''),
|
||||
|
|
@ -3114,8 +3146,10 @@ class AbdmGateway extends BaseController
|
|||
'observations' => $observations,
|
||||
'allergies' => $allergies,
|
||||
'care_plans' => $carePlans,
|
||||
'documents' => [],
|
||||
'skip_pdf' => true,
|
||||
'ui_course_treatment' => $courseRows,
|
||||
'ui_course_treatment_narrative' => $courseRemark,
|
||||
'documents' => $documents,
|
||||
'template' => $this->resolveAbdmDischargeTemplate(),
|
||||
];
|
||||
|
||||
$factory = new FhirGeneratorFactory();
|
||||
|
|
@ -3250,46 +3284,33 @@ class AbdmGateway extends BaseController
|
|||
$directory = WRITEPATH . 'uploads' . DIRECTORY_SEPARATOR . 'abdm' . DIRECTORY_SEPARATOR . 'ipd' . DIRECTORY_SEPARATOR . $ipdId;
|
||||
$createdAt = $this->toIsoDateTimeOrNow($dischargeRaw);
|
||||
|
||||
// Ensure IPD Discharge Summary PDF is cached if missing
|
||||
$summaryPath = $directory . DIRECTORY_SEPARATOR . 'discharge-summary.pdf';
|
||||
if (! is_file($summaryPath) || filesize($summaryPath) === 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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
// Always generate fresh IPD Discharge Summary PDF using the configured Online/ABDM template
|
||||
$abdmTemplate = $this->resolveAbdmDischargeTemplate();
|
||||
$abdmTemplateId = (int) ($abdmTemplate['id'] ?? 0);
|
||||
|
||||
// Ensure IPD Bill PDF is cached if missing
|
||||
$billPath = $directory . DIRECTORY_SEPARATOR . 'ipd-bill.pdf';
|
||||
if (! is_file($billPath) || filesize($billPath) === 0) {
|
||||
try {
|
||||
$billingCtrl = new \App\Controllers\Billing\Ipd();
|
||||
$request = \Config\Services::request();
|
||||
$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(),
|
||||
]);
|
||||
}
|
||||
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, $abdmTemplateId > 0 ? $abdmTemplateId : null);
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'Auto-generating IPD Discharge Summary PDF failed for IPD {ipd}: {msg}', [
|
||||
'ipd' => $ipdId,
|
||||
'msg' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
$documents = [];
|
||||
$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) {
|
||||
|
|
@ -3304,6 +3325,8 @@ class AbdmGateway extends BaseController
|
|||
$documents[] = [
|
||||
'title' => $definition['title'],
|
||||
'loinc_code' => $definition['loinc'],
|
||||
'snomed_code' => $definition['snomed'],
|
||||
'snomed_display' => $definition['snomed_display'] ?? 'Discharge summary',
|
||||
'content_type' => 'application/pdf',
|
||||
'data' => base64_encode($binary),
|
||||
'created_at' => $createdAt,
|
||||
|
|
@ -3314,6 +3337,17 @@ class AbdmGateway extends BaseController
|
|||
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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -315,7 +315,7 @@ class Ipd_discharge extends BaseController
|
|||
->getResultArray();
|
||||
}
|
||||
|
||||
private function resolveAbdmDischargeTemplate(int $requestedId = 0): array
|
||||
public function resolveAbdmDischargeTemplate(int $requestedId = 0): array
|
||||
{
|
||||
$templates = $this->getDischargeTemplateRows();
|
||||
if (empty($templates)) {
|
||||
|
|
@ -5756,6 +5756,62 @@ class Ipd_discharge extends BaseController
|
|||
'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()
|
||||
{
|
||||
$permission = $this->requireAnyPermission([
|
||||
|
|
@ -8030,266 +8086,28 @@ class Ipd_discharge extends BaseController
|
|||
|
||||
$this->createIpdDischargeWorkTask($ipdId, $panelData);
|
||||
|
||||
try {
|
||||
$headerHtml = '';
|
||||
if ($withHeader) {
|
||||
$configuredHeader = $this->applyDischargeTemplateTokens(
|
||||
trim((string) ($templateSettings['header_html'] ?? '')),
|
||||
$templateTokenVars
|
||||
);
|
||||
$headerHtml = $this->sanitizeDischargePdfHtml($configuredHeader, false);
|
||||
$headerHtml = mpdf_normalize_font_weight_css($headerHtml);
|
||||
}
|
||||
$pdfBinary = $this->generateDischargeSummaryPdfBinary($ipdId, $withHeader, $requestedTemplateId > 0 ? $requestedTemplateId : null);
|
||||
|
||||
$configuredFooter = $this->applyDischargeTemplateTokens(
|
||||
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();
|
||||
}
|
||||
|
||||
if (! is_string($pdfBinary) || $pdfBinary === '' || ! str_starts_with($pdfBinary, '%PDF-')) {
|
||||
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 $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.');
|
||||
}
|
||||
}
|
||||
}
|
||||
->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', [
|
||||
'ipd_id' => $ipdId,
|
||||
'ipd_info' => $panelData['ipd_info'] ?? null,
|
||||
'person_info' => $panelData['person_info'] ?? null,
|
||||
'content' => $renderedHtml,
|
||||
'selected_template_name' => (string) ($templatePack['selected_template_name'] ?? ''),
|
||||
'selected_template_id' => (int) ($templatePack['selected_template_id'] ?? 0),
|
||||
'print_type' => $printType,
|
||||
'print_mode' => 'standard',
|
||||
]);
|
||||
// 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);
|
||||
}
|
||||
|
||||
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) {
|
||||
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)
|
||||
? $templatePack['selected_template_settings']
|
||||
: $this->defaultDischargeTemplateSettings();
|
||||
|
|
@ -8745,15 +8568,15 @@ class Ipd_discharge extends BaseController
|
|||
trim((string) ($templateSettings['footer_html'] ?? '')),
|
||||
$templateTokenVars
|
||||
);
|
||||
$footerHtml = $this->sanitizeDischargePdfHtml($configuredFooter, false);
|
||||
$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'
|
||||
|
|
@ -8783,12 +8606,54 @@ class Ipd_discharge extends BaseController
|
|||
$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;
|
||||
$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);
|
||||
|
||||
|
|
@ -8814,41 +8679,28 @@ class Ipd_discharge extends BaseController
|
|||
$directory = WRITEPATH . 'uploads' . DIRECTORY_SEPARATOR . 'abdm' . DIRECTORY_SEPARATOR . 'ipd' . DIRECTORY_SEPARATOR . $ipdId;
|
||||
$createdAt = $this->toIsoDateTimeOrNow($dischargeRaw);
|
||||
|
||||
// Ensure IPD Discharge Summary PDF is cached if missing
|
||||
$summaryPath = $directory . DIRECTORY_SEPARATOR . 'discharge-summary.pdf';
|
||||
if (! is_file($summaryPath) || filesize($summaryPath) === 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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
// Always generate fresh IPD Discharge Summary PDF using the configured Online/ABDM template
|
||||
$abdmTemplate = $this->resolveAbdmDischargeTemplate();
|
||||
$abdmTemplateId = (int) ($abdmTemplate['id'] ?? 0);
|
||||
|
||||
// Ensure IPD Bill PDF is cached if missing
|
||||
$billPath = $directory . DIRECTORY_SEPARATOR . 'ipd-bill.pdf';
|
||||
if (! is_file($billPath) || filesize($billPath) === 0) {
|
||||
try {
|
||||
$billingCtrl = new \App\Controllers\Billing\Ipd();
|
||||
$request = \Config\Services::request();
|
||||
$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(),
|
||||
]);
|
||||
}
|
||||
try {
|
||||
$this->generateDischargeSummaryPdfBinary($ipdId, true, $abdmTemplateId > 0 ? $abdmTemplateId : null);
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'Auto-generating IPD Discharge Summary PDF failed for IPD {ipd}: {msg}', [
|
||||
'ipd' => $ipdId,
|
||||
'msg' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
$documents = [];
|
||||
$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) {
|
||||
|
|
@ -8864,6 +8716,7 @@ class Ipd_discharge extends BaseController
|
|||
'title' => $definition['title'],
|
||||
'loinc_code' => $definition['loinc'],
|
||||
'snomed_code' => $definition['snomed'],
|
||||
'snomed_display' => $definition['snomed_display'] ?? 'Discharge summary',
|
||||
'content_type' => 'application/pdf',
|
||||
'data' => base64_encode($binary),
|
||||
'created_at' => $createdAt,
|
||||
|
|
@ -9184,13 +9037,25 @@ class Ipd_discharge extends BaseController
|
|||
$abhaAddress = $abhaIdRaw;
|
||||
}
|
||||
|
||||
$admissionRaw = trim((string) ($ipdRow['register_date'] ?? ''));
|
||||
$dischargeRaw = trim((string) ($ipdRow['discharge_date'] ?? ''));
|
||||
$admissionIso = $this->toIsoDateTimeOrNow($admissionRaw);
|
||||
$dischargeIso = $this->toIsoDateTimeOrNow($dischargeRaw);
|
||||
$visitDate = $dischargeRaw !== ''
|
||||
? date('Y-m-d', strtotime($dischargeRaw))
|
||||
: ($admissionRaw !== '' ? date('Y-m-d', strtotime($admissionRaw)) : date('Y-m-d'));
|
||||
$admissionDateRaw = trim((string) ($ipdRow['register_date'] ?? ''));
|
||||
$admissionTimeRaw = trim((string) ($ipdRow['reg_time'] ?? ''));
|
||||
$admissionCombined = $admissionDateRaw;
|
||||
if ($admissionCombined !== '' && $admissionTimeRaw !== '') {
|
||||
$admissionCombined .= ' ' . $admissionTimeRaw;
|
||||
}
|
||||
|
||||
$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);
|
||||
$conditionRows = [];
|
||||
|
|
@ -9200,6 +9065,12 @@ class Ipd_discharge extends BaseController
|
|||
$conditionRows[] = ['text' => $text, 'code' => ''];
|
||||
}
|
||||
}
|
||||
if (empty($conditionRows) && ! empty($ipdRow['problem'])) {
|
||||
$prob = trim((string) $ipdRow['problem']);
|
||||
if ($this->isMeaningfulDischargeValue($prob)) {
|
||||
$conditionRows[] = ['text' => $prob, 'code' => ''];
|
||||
}
|
||||
}
|
||||
|
||||
$chiefComplaintsList = [];
|
||||
if ($this->tableHasColumns('ipd_discharge_complaint', ['ipd_id', 'comp_report'])) {
|
||||
|
|
@ -9446,6 +9317,27 @@ class Ipd_discharge extends BaseController
|
|||
}
|
||||
$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);
|
||||
$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));
|
||||
$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 = [
|
||||
'record_id' => (string) $ipdId,
|
||||
'bundle_identifier' => 'discharge-' . $ipdNo,
|
||||
|
|
@ -9470,6 +9369,8 @@ class Ipd_discharge extends BaseController
|
|||
'completed_at' => $dischargeIso,
|
||||
'department' => trim((string) ($ipdRow['dept_id'] ?? '')),
|
||||
'doctor_name' => trim((string) ($ipdRow['r_doc_name'] ?? '')),
|
||||
'discharge_status' => $dischargeStatus,
|
||||
'problem' => trim((string) ($ipdRow['problem'] ?? '')),
|
||||
'organization' => [
|
||||
'id' => $hfrId,
|
||||
'name' => $hospitalName,
|
||||
|
|
@ -9480,6 +9381,13 @@ class Ipd_discharge extends BaseController
|
|||
'name' => $patientName,
|
||||
'gender' => $this->normalizeFhirGender($genderRaw),
|
||||
'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_address' => $abhaAddress,
|
||||
],
|
||||
|
|
@ -9489,6 +9397,7 @@ class Ipd_discharge extends BaseController
|
|||
'class_code' => 'IMP',
|
||||
'start' => $admissionIso,
|
||||
'end' => $dischargeIso,
|
||||
'discharge_disposition' => $dischargeStatus,
|
||||
'location_display' => (string) ($locationInfo['display'] ?? ''),
|
||||
'ward' => (string) ($locationInfo['ward'] ?? ''),
|
||||
'room' => (string) ($locationInfo['room'] ?? ''),
|
||||
|
|
@ -9502,11 +9411,11 @@ class Ipd_discharge extends BaseController
|
|||
'ui_surgeries' => $procedureRows,
|
||||
'ui_final_diagnosis' => $conditionRows,
|
||||
'ui_summary_investigation' => $uiSummaryInvestigation,
|
||||
'ui_course_treatment' => $courseRows,
|
||||
'ui_course_treatment' => $courseRows,
|
||||
'ui_course_treatment_narrative' => $courseRemark,
|
||||
'ui_discharge_medicine' => $medicationRows,
|
||||
'allergies' => $allergyRows,
|
||||
'documents' => [],
|
||||
'skip_pdf' => true,
|
||||
'documents' => $documentsList,
|
||||
'template' => $this->resolveAbdmDischargeTemplate(),
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -287,7 +287,7 @@ class FhirDocumentBuilder
|
|||
{
|
||||
$out = [];
|
||||
foreach ($data as $key => $value) {
|
||||
if ($value === null || $value === '') {
|
||||
if ($value === null || $value === '' || $this->isPlaceholderValue($value)) {
|
||||
continue;
|
||||
}
|
||||
if (is_array($value)) {
|
||||
|
|
|
|||
|
|
@ -83,6 +83,71 @@ abstract class AbstractModuleFhirGenerator
|
|||
$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'] ?? ''));
|
||||
if (is_string($abhaId) && $abhaId !== '') {
|
||||
$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'] ?? ''));
|
||||
if ($this->isMeaningfulValue($uhid)) {
|
||||
$patient['identifier'][] = [
|
||||
|
|
@ -177,7 +255,10 @@ abstract class AbstractModuleFhirGenerator
|
|||
];
|
||||
|
||||
if ($encounterRef !== null) {
|
||||
$composition['encounter'] = ['reference' => $encounterRef];
|
||||
$composition['encounter'] = [
|
||||
'reference' => $encounterRef,
|
||||
'display' => 'Encounter',
|
||||
];
|
||||
}
|
||||
|
||||
return $composition;
|
||||
|
|
@ -231,6 +312,9 @@ abstract class AbstractModuleFhirGenerator
|
|||
|
||||
$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 = [
|
||||
'resourceType' => 'Encounter',
|
||||
'id' => 'encounter-' . $id,
|
||||
|
|
@ -239,7 +323,7 @@ abstract class AbstractModuleFhirGenerator
|
|||
]],
|
||||
'text' => [
|
||||
'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,
|
||||
'status' => 'finished',
|
||||
|
|
@ -260,17 +344,74 @@ abstract class AbstractModuleFhirGenerator
|
|||
'dischargeDisposition' => [
|
||||
'coding' => [[
|
||||
'system' => 'http://terminology.hl7.org/CodeSystem/discharge-disposition',
|
||||
'code' => 'home',
|
||||
'display' => 'Home',
|
||||
'code' => $disposition['code'],
|
||||
'display' => $disposition['display'],
|
||||
]],
|
||||
'text' => 'Discharged to Home Care',
|
||||
]
|
||||
'text' => $disposition['text'],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
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 */
|
||||
protected function buildPractitioner(array $source): ?array
|
||||
{
|
||||
|
|
|
|||
|
|
@ -148,6 +148,31 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
|
|||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -407,8 +432,30 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
|
|||
}
|
||||
|
||||
|
||||
// ── Care Plans / Discharge Advice ──────────────────────────────────────
|
||||
foreach ((array) ($source['ui_discharge_summary'] ?? $source['care_plans'] ?? []) as $idx => $cp) {
|
||||
// ── Care Plans / Discharge Advice & Hospital Course ────────────────────
|
||||
$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'));
|
||||
$description = $this->cleanPlainText((string) ($cp['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];
|
||||
}
|
||||
|
||||
// Care plans / Discharge advice
|
||||
$carePlans = (array) ($source['ui_discharge_summary'] ?? $source['care_plans'] ?? []);
|
||||
if (! empty($carePlans)) {
|
||||
$sectionDefinitions[] = ['title' => 'Care Plan', 'code' => '734163000', 'display' => 'Care plan', 'prefix' => 'discharge-careplan-', 'items' => $carePlans];
|
||||
// Care plans / Discharge advice & hospital course
|
||||
if (! empty($carePlansList)) {
|
||||
$sectionDefinitions[] = ['title' => 'Care Plan', 'code' => '734163000', 'display' => 'Care plan', 'prefix' => 'discharge-careplan-', 'items' => $carePlansList];
|
||||
}
|
||||
|
||||
// Document Reference
|
||||
|
|
@ -865,13 +911,13 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
|
|||
{
|
||||
$hospitalName = trim((string) ($source['organization']['name'] ?? 'Hospital'));
|
||||
$patientName = trim((string) ($source['patient']['name'] ?? 'Patient'));
|
||||
$patientId = trim((string) ($source['patient']['id'] ?? 'N/A'));
|
||||
$gender = ucfirst(trim((string) ($source['patient']['gender'] ?? 'N/A')));
|
||||
$dob = trim((string) ($source['patient']['dob'] ?? 'N/A'));
|
||||
$abhaId = trim((string) ($source['patient']['abha_id'] ?? 'N/A'));
|
||||
$recordId = trim((string) ($source['record_id'] ?? 'N/A'));
|
||||
$patientId = trim((string) ($source['patient']['id'] ?? ''));
|
||||
$gender = ucfirst(trim((string) ($source['patient']['gender'] ?? '')));
|
||||
$dob = trim((string) ($source['patient']['dob'] ?? ''));
|
||||
$abhaId = trim((string) ($source['patient']['abha_id'] ?? ''));
|
||||
$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');
|
||||
$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),
|
||||
'{{UHID}}' => htmlspecialchars($patientId),
|
||||
'{{IPD_CODE}}' => htmlspecialchars($recordId),
|
||||
'{{AGE_GENDER}}' => htmlspecialchars($gender . ($dob !== 'N/A' ? ' / ' . $dob : '')),
|
||||
'{{AGE_GENDER}}' => htmlspecialchars(trim($gender . ($dob !== '' ? ' / ' . $dob : ''))),
|
||||
'{{ADMIT_DATE}}' => htmlspecialchars($admDate),
|
||||
'{{DISCHARGE_DATE}}' => htmlspecialchars($disDate),
|
||||
'{{DOCTOR_NAMES}}' => htmlspecialchars($doctorName),
|
||||
|
|
@ -1229,11 +1275,64 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
|
|||
string $hospitalName,
|
||||
array $sections
|
||||
): string {
|
||||
$ipdNo = (string) ($source['encounter']['ipd_no'] ?? $source['record_id'] ?? '');
|
||||
$admitDate = (string) ($source['encounter']['start'] ?? $source['admission_date'] ?? '');
|
||||
$dischargeDate = (string) ($source['encounter']['end'] ?? $source['discharge_date'] ?? $source['completed_at'] ?? '');
|
||||
$locDisplay = (string) ($source['encounter']['location_display'] ?? '');
|
||||
$abhaId = (string) ($source['patient']['abha_id'] ?? '');
|
||||
$ipdNo = trim((string) ($source['encounter']['ipd_no'] ?? $source['record_id'] ?? ''));
|
||||
$admitRaw = trim((string) ($source['encounter']['start'] ?? $source['admission_date'] ?? ''));
|
||||
$dischargeRaw = trim((string) ($source['encounter']['end'] ?? $source['discharge_date'] ?? $source['completed_at'] ?? ''));
|
||||
|
||||
$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 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 .= '<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>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>';
|
||||
if ($locDisplay !== '' || $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>Patient:</strong> ' . htmlspecialchars($patientName) . '</td><td style="border:1px solid #cbd5e1;padding:6px;"><strong>IPD No:</strong> ' . htmlspecialchars($ipdNo !== '' ? $ipdNo : ('IPD-' . $patientId)) . '</td></tr>';
|
||||
|
||||
$genderDob = htmlspecialchars(ucfirst($gender)) . ($dob !== '' ? (' / ' . htmlspecialchars($dob)) : '');
|
||||
$idCell = $identifierDisplay !== '' ? ('<strong>ABHA / ID:</strong> ' . htmlspecialchars($identifierDisplay)) : ('<strong>Doctor:</strong> ' . htmlspecialchars($drName));
|
||||
$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>';
|
||||
|
||||
|
|
|
|||
|
|
@ -31,9 +31,21 @@
|
|||
<tr>
|
||||
<td><?= (int) $i + 1 ?></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_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>
|
||||
<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>
|
||||
|
|
@ -150,5 +162,51 @@
|
|||
}).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>
|
||||
|
|
|
|||
|
|
@ -800,4 +800,128 @@ final class FhirGeneratorsTest extends CIUnitTestCase
|
|||
$this->assertContains('Medications', $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