diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 5f4ba29..f091e78 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -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'); diff --git a/app/Controllers/AbdmGateway.php b/app/Controllers/AbdmGateway.php index e417833..2b3b2c9 100644 --- a/app/Controllers/AbdmGateway.php +++ b/app/Controllers/AbdmGateway.php @@ -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|null */ diff --git a/app/Controllers/Ipd_discharge.php b/app/Controllers/Ipd_discharge.php index dbfce65..ec47489 100644 --- a/app/Controllers/Ipd_discharge.php +++ b/app/Controllers/Ipd_discharge.php @@ -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 = '
Page {PAGENO}/{nbpg}
'; - } - - $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 .= '' . "\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('/]*>[\s\S]*?<\/style>/i', '', $safeBody); - $safeBody = (string) preg_replace('/]*>[\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('
Page {PAGENO}/{nbpg}
'); - $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('/]*>[\s\S]*?<\/style>/i', '', $plainSourceHtml); - $plainSourceHtml = (string) preg_replace('/]*>[\s\S]*?<\/script>/i', '', $plainSourceHtml); - $plainSource = strip_tags($plainSourceHtml); - $plainSource = $this->forceValidUtf8($plainSource); - $plainSource = preg_replace('/\R{3,}/', "\n\n", (string) $plainSource) ?? ''; - - $plainHtml = '' - . '

Discharge Summary

' - . '
'
-                            . htmlspecialchars($plainSource, ENT_QUOTES, 'UTF-8')
-                            . '
'; - - $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('
Page {PAGENO}/{nbpg}
'); - $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 = '
Page {PAGENO}/{nbpg}
'; } - $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 = '' . "\n" . $pdfHtml; + $pdfHtml .= '' . "\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(), ]; diff --git a/app/Libraries/Abdm/Fhir/FhirDocumentBuilder.php b/app/Libraries/Abdm/Fhir/FhirDocumentBuilder.php index addac8f..d5c509b 100644 --- a/app/Libraries/Abdm/Fhir/FhirDocumentBuilder.php +++ b/app/Libraries/Abdm/Fhir/FhirDocumentBuilder.php @@ -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)) { diff --git a/app/Libraries/Abdm/Fhir/Generators/AbstractModuleFhirGenerator.php b/app/Libraries/Abdm/Fhir/Generators/AbstractModuleFhirGenerator.php index b9d030a..e622465 100644 --- a/app/Libraries/Abdm/Fhir/Generators/AbstractModuleFhirGenerator.php +++ b/app/Libraries/Abdm/Fhir/Generators/AbstractModuleFhirGenerator.php @@ -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' => '

Encounter: ' . $this->escapeXhtml($classDisplay) . ' ' . $this->escapeXhtml($id) . '

', + 'div' => '

Encounter: ' . $this->escapeXhtml($classDisplay) . ' ' . $this->escapeXhtml($id) . ' - ' . $this->escapeXhtml($disposition['text']) . '

', ], '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 $source */ protected function buildPractitioner(array $source): ?array { diff --git a/app/Libraries/Abdm/Fhir/Generators/DischargeFhirGenerator.php b/app/Libraries/Abdm/Fhir/Generators/DischargeFhirGenerator.php index 7024830..2a14215 100644 --- a/app/Libraries/Abdm/Fhir/Generators/DischargeFhirGenerator.php +++ b/app/Libraries/Abdm/Fhir/Generators/DischargeFhirGenerator.php @@ -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 = '
'; $html .= '
'; @@ -1242,11 +1341,22 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac $html .= '
'; $html .= ''; - $html .= ''; - $html .= ''; - $html .= ''; - if ($locDisplay !== '' || $drName !== '') { - $html .= ''; + $html .= ''; + + $genderDob = htmlspecialchars(ucfirst($gender)) . ($dob !== '' ? (' / ' . htmlspecialchars($dob)) : ''); + $idCell = $identifierDisplay !== '' ? ('ABHA / ID: ' . htmlspecialchars($identifierDisplay)) : ('Doctor: ' . htmlspecialchars($drName)); + $html .= ''; + + $html .= ''; + + if ($diagnosisSummary !== '') { + $html .= ''; + } + + $html .= ''; + + if ($patientMobile !== '') { + $html .= ''; } $html .= '
Patient: ' . htmlspecialchars($patientName) . 'IPD No: ' . htmlspecialchars($ipdNo) . '
Gender / DOB: ' . htmlspecialchars(ucfirst($gender)) . ($dob !== '' ? (' / ' . htmlspecialchars($dob)) : '') . 'ABHA Number: ' . htmlspecialchars($abhaId !== '' ? $abhaId : 'N/A') . '
Admission Date: ' . htmlspecialchars($admitDate) . 'Discharge Date: ' . htmlspecialchars($dischargeDate) . '
Location / Bed: ' . htmlspecialchars($locDisplay !== '' ? $locDisplay : 'General Ward') . 'Doctor: ' . htmlspecialchars($drName) . '
Patient: ' . htmlspecialchars($patientName) . 'IPD No: ' . htmlspecialchars($ipdNo !== '' ? $ipdNo : ('IPD-' . $patientId)) . '
Gender / DOB: ' . $genderDob . '' . $idCell . '
Admission Date: ' . htmlspecialchars($admitDisplay) . 'Discharge Date: ' . htmlspecialchars($dischargeDisplay) . '
Final Diagnosis: ' . htmlspecialchars($diagnosisSummary) . '
Outcome / Status: ' . htmlspecialchars($outcomeText) . 'Doctor: ' . htmlspecialchars($drName) . ($locDisplay !== '' ? (' | Location: ' . htmlspecialchars($locDisplay)) : '') . '
Contact: ' . htmlspecialchars($patientMobile) . '
'; diff --git a/app/Views/ipd_discharge/discharge_template_list.php b/app/Views/ipd_discharge/discharge_template_list.php index cbebae3..4144310 100644 --- a/app/Views/ipd_discharge/discharge_template_list.php +++ b/app/Views/ipd_discharge/discharge_template_list.php @@ -31,9 +31,21 @@ - Yes' : '' ?> + + + Yes + + + + Audit' : '' ?> - Online / ABDM' : '' ?> + + + Online / ABDM + + + + Edit @@ -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('', { + 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('?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('', { + 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('?mode=list', 'IPD Discharge Templates'); + } else { + showMsg(data.error_text || 'Unable to update default template.', false); + } + }).catch(function () { showMsg('Request failed.', false); }); + }); + }); })(); diff --git a/tests/unit/Abdm/FhirGeneratorsTest.php b/tests/unit/Abdm/FhirGeneratorsTest.php index 2ec1c99..3a552c9 100644 --- a/tests/unit/Abdm/FhirGeneratorsTest.php +++ b/tests/unit/Abdm/FhirGeneratorsTest.php @@ -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*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']); + } } +