diff --git a/app/Controllers/AbdmGateway.php b/app/Controllers/AbdmGateway.php index a657a68..6eefcc4 100644 --- a/app/Controllers/AbdmGateway.php +++ b/app/Controllers/AbdmGateway.php @@ -2747,22 +2747,22 @@ class AbdmGateway extends BaseController } $medications = []; - $legacyMeds = $this->ipdRows('ipd_discharge_prescrption_prescribed', ['med_name', 'dosage', 'dosage_when', 'dosage_freq', 'no_of_days'], $ipdId); + $legacyMeds = $this->ipdRows('ipd_discharge_prescrption_prescribed', ['med_name', 'dosage', 'dosage_when', 'dosage_freq', 'no_of_days', 'remark'], $ipdId); if (empty($legacyMeds)) { - $legacyMeds = $this->ipdRows('ipd_discharge_prescription_prescribed', ['med_name', 'dosage', 'dosage_when', 'dosage_freq', 'no_of_days'], $ipdId); + $legacyMeds = $this->ipdRows('ipd_discharge_prescription_prescribed', ['med_name', 'dosage', 'dosage_when', 'dosage_freq', 'no_of_days', 'remark'], $ipdId); } foreach ($legacyMeds as $row) { $name = trim((string) ($row['med_name'] ?? '')); if ($name === '') { continue; } - $dosageText = $this->resolveDischargeMedDosageLabel( - (int) ($row['dosage'] ?? 0), - (int) ($row['dosage_when'] ?? 0), - (int) ($row['dosage_freq'] ?? 0) + $dosage = $this->resolveDischargeMedDosageLabel( + $row['dosage'] ?? '', + $row['dosage_when'] ?? '', + $row['dosage_freq'] ?? '', + (string) ($row['no_of_days'] ?? ''), + (string) ($row['remark'] ?? '') ); - $days = trim((string) ($row['no_of_days'] ?? '')); - $dosage = trim($dosageText . (($days !== '' && $days !== '0') ? ' for ' . $days . ' days' : '')); $medications[] = ['name' => $name, 'dosage' => $dosage]; } if (empty($medications)) { @@ -2861,6 +2861,51 @@ class AbdmGateway extends BaseController ]; } + // Dietary Advice from food items + $foodIds = is_array($instructionMeta['food_ids'] ?? null) ? ($instructionMeta['food_ids'] ?? []) : []; + if (empty($foodIds) && $this->db->tableExists('ipd_discharge_drug_food_interaction')) { + $legacyFoodRows = $this->ipdRows('ipd_discharge_drug_food_interaction', ['food_id_list'], $ipdId); + if (! empty($legacyFoodRows)) { + $foodIds = array_values(array_filter(array_map('trim', explode(',', (string) ($legacyFoodRows[0]['food_id_list'] ?? ''))), 'strlen')); + } + } + if (! empty($foodIds) && $this->db->tableExists('ipd_discharge_master_food')) { + $foodRows = $this->db->table('ipd_discharge_master_food') + ->select('id,food_short,food_desc,food_desc_lang') + ->whereIn('id', array_map('intval', $foodIds)) + ->get() + ->getResultArray(); + $foodMap = []; + foreach ($foodRows as $fr) { + $foodMap[(int) $fr['id']] = $fr; + } + $foodLines = []; + $fIndex = 1; + foreach ($foodIds as $fid) { + $fr = $foodMap[(int) $fid] ?? null; + if (! $fr) { + continue; + } + $heading = trim((string) ($fr['food_short'] ?? '')); + $line = trim((string) ($fr['food_desc_lang'] ?? '')); + if ($line === '') { + $line = trim((string) ($fr['food_desc'] ?? '')); + } + if ($heading === '' && $line === '') { + continue; + } + $foodLines[] = $fIndex . '. ' . ($heading !== '' ? $heading . ': ' : '') . $line; + $fIndex++; + } + if (! empty($foodLines)) { + $carePlans[] = [ + 'title' => 'Dietary Advice', + 'description' => implode("\n", $foodLines), + 'created_at' => $this->toIsoDateTimeOrNow($dischargeRaw), + ]; + } + } + $otherAdvice = trim((string) ($instructionMeta['other_text'] ?? '')); if ($otherAdvice !== '') { $carePlans[] = [ @@ -2872,9 +2917,16 @@ class AbdmGateway extends BaseController $reviewAfter = trim((string) ($instructionRow['review_after'] ?? '')); if ($reviewAfter !== '') { + $reviewDate = ''; + if ($dischargeRaw !== '' && is_numeric($reviewAfter)) { + $reviewTs = strtotime($dischargeRaw . ' +' . (int) $reviewAfter . ' days'); + if ($reviewTs !== false) { + $reviewDate = ' (' . date('d-m-Y', $reviewTs) . ')'; + } + } $carePlans[] = [ 'title' => 'Follow Up', - 'description' => 'Review After: ' . $reviewAfter, + 'description' => 'Review After: ' . $reviewAfter . ' Days' . $reviewDate . ' / as and when required', 'created_at' => $this->toIsoDateTimeOrNow($dischargeRaw), ]; } @@ -2963,10 +3015,27 @@ class AbdmGateway extends BaseController { $directory = WRITEPATH . 'uploads' . DIRECTORY_SEPARATOR . 'abdm' . DIRECTORY_SEPARATOR . 'ipd' . DIRECTORY_SEPARATOR . $ipdId; $createdAt = $this->toIsoDateTimeOrNow($dischargeRaw); + + // Ensure IPD Bill PDF is cached if missing + $billPath = $directory . DIRECTORY_SEPARATOR . 'ipd-bill.pdf'; + if (! is_file($billPath) || filesize($billPath) === 0) { + try { + (new \App\Controllers\Billing\Ipd())->generateIpdBillPdfBinary($ipdId, 1, true); + } catch (\Throwable $e) { + log_message('error', 'Auto-generating IPD Bill PDF failed for IPD {ipd}: {msg}', [ + 'ipd' => $ipdId, + 'msg' => $e->getMessage(), + ]); + } + } + $documents = []; - foreach ([ - ['file' => 'discharge-summary.pdf', 'title' => 'IPD Discharge Summary', 'loinc' => '18842-5'], - ] as $definition) { + $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'], + ]; + + foreach ($definitions as $definition) { $path = $directory . DIRECTORY_SEPARATOR . $definition['file']; if (! is_file($path) || filesize($path) === 0) { continue; @@ -3208,38 +3277,187 @@ class AbdmGateway extends BaseController return ! in_array($normalized, ['-', '--', 'na', 'n/a', 'nil', 'none', 'not done'], true); } - /** Resolve dose/when/freq IDs to a human-readable label for FHIR dosage text. */ - private function resolveDischargeMedDosageLabel(int $doseId, int $whenId, int $freqId): string + /** Resolve dose/when/freq/days to a comprehensive human-readable label for FHIR dosage text. */ + private function resolveDischargeMedDosageLabel($doseVal, $whenVal, $freqVal, string $days = '', string $remark = ''): string { + static $whenCodeDescMap = [ + 'BF' => 'BF (BEFORE FOOD)', 'AF' => 'AF (AFTER FOOD)', + 'WF' => 'WF (WITH FOOD)', 'ES' => 'ES (EMPTY STOMACH)', + 'BBF' => 'BBF (BEFORE BREAKFAST)', 'ABF' => 'ABF (AFTER BREAKFAST)', + 'BL' => 'BL (BEFORE LUNCH)', 'AL' => 'AL (AFTER LUNCH)', + 'BD' => 'BD (BEFORE DINNER)', 'AD' => 'AD (AFTER DINNER)', + 'BT' => 'BT (BED TIME)', + ]; + static $whenHindiMap = [ + 'BF' => 'भोजन से पहले', 'BEFORE FOOD' => 'भोजन से पहले', + 'AF' => 'भोजन के बाद', 'AFTER FOOD' => 'भोजन के बाद', + 'WF' => 'भोजन के साथ', 'WITH FOOD' => 'भोजन के साथ', + 'ES' => 'सुबह खाली पेट', 'EMPTY STOMACH' => 'सुबह खाली पेट', + 'BBF'=> 'नाश्ते से पहले', 'BEFORE BREAKFAST' => 'नाश्ते से पहले', + 'ABF'=> 'नाश्ते के बाद', 'AFTER BREAKFAST' => 'नाश्ते के बाद', + 'BL' => 'दोपहर के भोजन से पहले', 'BEFORE LUNCH' => 'दोपहर के भोजन से पहले', + 'AL' => 'दोपहर के भोजन के बाद', 'AFTER LUNCH' => 'दोपहर के भोजन के बाद', + 'BD' => 'रात के भोजन से पहले', 'BEFORE DINNER'=> 'रात के भोजन से पहले', + 'AD' => 'रात के भोजन के बाद', 'AFTER DINNER' => 'रात के भोजन के बाद', + 'BT' => 'रात को सोते समय', 'BED TIME' => 'रात को सोते समय', + ]; + static $freqHindiMap = [ + 'OD' => 'दिन में एक बार (OD)', 'BD' => 'दिन में दो बार (BD)', + 'TDS' => 'दिन में तीन बार (TDS)', 'TID' => 'दिन में तीन बार (TID)', + 'QID' => 'दिन में चार बार (QID)', 'HS' => 'रात को सोते समय (HS)', + 'SOS' => 'ज़रूरत पड़ने पर (SOS)', 'STAT'=> 'तुरंत एक बार (STAT)', + 'ALTERNATE DAY' => 'एक दिन छोड़कर', + 'DAILY' => 'प्रतिदिन', + 'WEEKLY' => 'हफ़्ते में एक बार', + 'MONTHLY' => 'महीने में एक बार', + ]; + static $remarkHindiMap = [ + 'TAKE WITH MILK' => 'दूध के साथ लें', + 'TAKE WITH WARM WATER' => 'गुनगुने पानी के साथ लें', + 'AVOID SOUR FOOD AND DAIRY PRODUCTS'=> 'खट्टा और डेयरी उत्पाद न लें', + 'TAKE AFTER MEALS' => 'भोजन के बाद लें', + 'TAKE ON AN EMPTY STOMACH EARLY MORNING' => 'सुबह खाली पेट लें', + 'CHEW WELL BEFORE SWALLOWING' => 'चबाकर खाएं', + 'DISSOLVE IN HALF GLASS OF WATER' => 'आधे गिलास पानी में घोलकर लें', + 'APPLY LOCALLY TWICE DAILY' => 'दिन में दो बार लगाएं', + 'DO NOT CRUSH OR CHEW TABLET' => 'गोली को तोड़े या चबाएं नहीं', + 'AVOID ALCOHOL WHILE TAKING THIS MEDICINE' => 'शराब का सेवन न करें', + 'COMPLETE FULL COURSE OF ANTIBIOTICS' => 'एंटीबायोटिक का पूरा कोर्स लें', + 'DRINK PLENTY OF FLUIDS / WATER' => 'प्रचुर मात्रा में पानी पिएं', + ]; + static $doseCache = null; static $whenCache = null; static $freqCache = null; - $load = function (string $table) { - $map = []; - if (! $this->db->tableExists($table)) { - return $map; + static $whenHindiCache = null; + static $freqHindiCache = null; + + if ($doseCache === null && $this->db->tableExists('opd_dose_shed')) { + $doseCache = []; + foreach ($this->db->table('opd_dose_shed')->select('id, dose_desc')->get()->getResultArray() as $r) { + $doseCache[(int) $r['id']] = trim((string) ($r['dose_desc'] ?? '')); } - $fields = $this->db->getFieldNames($table) ?? []; - $labelField = in_array('dose_desc', $fields, true) ? 'dose_desc' - : (in_array('freq_desc', $fields, true) ? 'freq_desc' - : (in_array('when_desc', $fields, true) ? 'when_desc' : null)); - if ($labelField === null) { - return $map; + } + if ($whenCache === null && $this->db->tableExists('opd_dose_when')) { + $whenCache = []; + $whenHindiCache = []; + $fields = $this->db->getFieldNames('opd_dose_when') ?? []; + $hasHindi = in_array('dose_sign_hindi', $fields, true); + foreach ($this->db->table('opd_dose_when')->select('id, dose_sign' . ($hasHindi ? ', dose_sign_hindi' : ''))->get()->getResultArray() as $r) { + $whenCache[(int) $r['id']] = trim((string) ($r['dose_sign'] ?? '')); + if ($hasHindi) { + $whenHindiCache[(int) $r['id']] = trim((string) ($r['dose_sign_hindi'] ?? '')); + } } - foreach ($this->db->table($table)->select('id,' . $labelField)->get()->getResultArray() as $r) { - $map[(int) $r['id']] = trim((string) $r[$labelField]); + } + if ($freqCache === null && $this->db->tableExists('opd_dose_frequency')) { + $freqCache = []; + $freqHindiCache = []; + $fields = $this->db->getFieldNames('opd_dose_frequency') ?? []; + $hasHindi = in_array('dose_sign_hindi', $fields, true); + foreach ($this->db->table('opd_dose_frequency')->select('id, dose_sign' . ($hasHindi ? ', dose_sign_hindi' : ''))->get()->getResultArray() as $r) { + $freqCache[(int) $r['id']] = trim((string) ($r['dose_sign'] ?? '')); + if ($hasHindi) { + $freqHindiCache[(int) $r['id']] = trim((string) ($r['dose_sign_hindi'] ?? '')); + } } - return $map; - }; - if ($doseCache === null) { $doseCache = $load('opd_dose_shed'); } - if ($whenCache === null) { $whenCache = $load('opd_dose_when'); } - if ($freqCache === null) { $freqCache = $load('opd_dose_frequency'); } - $parts = array_filter([ - $doseId > 0 ? ($doseCache[$doseId] ?? '') : '', - $whenId > 0 ? ($whenCache[$whenId] ?? '') : '', - $freqId > 0 ? ($freqCache[$freqId] ?? '') : '', - ], fn ($v) => $v !== ''); - return implode(' ', $parts); + } + + $doseText = ''; + if (is_numeric($doseVal) && (int) $doseVal > 0) { + $doseText = $doseCache[(int) $doseVal] ?? ''; + } elseif (is_string($doseVal) && trim($doseVal) !== '' && trim($doseVal) !== '0') { + $doseText = trim($doseVal); + } + + $whenText = ''; + $whenHindi = ''; + if (is_numeric($whenVal) && (int) $whenVal > 0) { + $whenText = $whenCache[(int) $whenVal] ?? ''; + $whenHindi = $whenHindiCache[(int) $whenVal] ?? ''; + } elseif (is_string($whenVal) && trim($whenVal) !== '' && trim($whenVal) !== '0') { + $whenText = trim($whenVal); + } + if ($whenText !== '') { + $upperWhen = strtoupper($whenText); + if (isset($whenCodeDescMap[$upperWhen])) { + $whenText = $whenCodeDescMap[$upperWhen]; + } + if ($whenHindi === '' && isset($whenHindiMap[$upperWhen])) { + $whenHindi = $whenHindiMap[$upperWhen]; + } + } + + $freqText = ''; + $freqHindi = ''; + if (is_numeric($freqVal) && (int) $freqVal > 0) { + $freqText = $freqCache[(int) $freqVal] ?? ''; + $freqHindi = $freqHindiCache[(int) $freqVal] ?? ''; + } elseif (is_string($freqVal) && trim($freqVal) !== '' && trim($freqVal) !== '0') { + $freqText = trim($freqVal); + } + if ($freqText !== '') { + $upperFreq = strtoupper($freqText); + if ($freqHindi === '' && isset($freqHindiMap[$upperFreq])) { + $freqHindi = $freqHindiMap[$upperFreq]; + } + } + + $cleanRemark = (string) preg_replace('/^(edit\s*remove|remove\s*edit|edit|remove|delete)\s*$/i', '', trim($remark)); + $cleanRemark = trim($cleanRemark); + $remarkHindi = ''; + if ($cleanRemark !== '') { + $upperRemark = strtoupper($cleanRemark); + if (isset($remarkHindiMap[$upperRemark])) { + $remarkHindi = $remarkHindiMap[$upperRemark]; + } + } + + $dirParts = []; + $dirLocalParts = []; + + if ($whenText !== '') { + $dirParts[] = $whenText; + } + if ($whenHindi !== '') { + $dirLocalParts[] = $whenHindi; + } + + if ($doseText !== '') { + $dirParts[] = $doseText; + } + + if ($freqText !== '') { + $dirParts[] = $freqText; + } + if ($freqHindi !== '') { + $dirLocalParts[] = $freqHindi; + } + + if ($cleanRemark !== '') { + $dirParts[] = $cleanRemark; + } + if ($remarkHindi !== '') { + $dirLocalParts[] = $remarkHindi; + } + + $daysClean = trim($days); + if ($daysClean !== '' && $daysClean !== '0') { + $daysNum = is_numeric($daysClean) ? (int) $daysClean : 0; + $dirParts[] = $daysNum > 0 ? ('for ' . $daysNum . ' days') : ('for ' . $daysClean); + if ($daysNum > 0) { + $dirLocalParts[] = $daysNum . ' दिन'; + } + } + + $engText = implode(' | ', array_values(array_unique(array_filter($dirParts)))); + $hinText = implode(' | ', array_values(array_unique(array_filter($dirLocalParts)))); + + if ($engText !== '' && $hinText !== '') { + return $engText . ' | ' . $hinText; + } + + return $engText !== '' ? $engText : $hinText; } private function normalizeFhirGender(string $gender): string diff --git a/app/Controllers/Billing/Ipd.php b/app/Controllers/Billing/Ipd.php index 684ced0..ef22eaa 100644 --- a/app/Controllers/Billing/Ipd.php +++ b/app/Controllers/Billing/Ipd.php @@ -941,9 +941,34 @@ class Ipd extends BaseController return $permission; } + $showHeader = $this->request->getGet('header') !== '0'; + $pdfBinary = $this->generateIpdBillPdfBinary($ipdId, $mode, $showHeader); + if ($pdfBinary === null) { + return $this->response->setStatusCode(404)->setBody('IPD not found'); + } + + $panelData = $this->ipdModel->getIpdPanelInfo($ipdId); + $ipdCode = (string) ($panelData['ipd_info']->ipd_code ?? ('IPD-' . $ipdId)); + $fileName = 'IPD_Bill_' . preg_replace('/[^A-Za-z0-9_\-]/', '_', $ipdCode) . '_M' . $mode . '.pdf'; + + return $this->response + ->setHeader('Content-Type', 'application/pdf') + ->setHeader('Content-Disposition', 'inline; filename="' . $fileName . '"') + ->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') + ->setHeader('Pragma', 'no-cache') + ->setHeader('Expires', '0') + ->setBody($pdfBinary); + } + + public function generateIpdBillPdfBinary(int $ipdId, int $mode = 1, bool $showHeader = true): ?string + { + if ($ipdId <= 0) { + return null; + } + $panelData = $this->ipdModel->getIpdPanelInfo($ipdId); if (empty($panelData)) { - return $this->response->setStatusCode(404)->setBody('IPD not found'); + return null; } // Recalculate before print so PDF always reflects latest bill math. @@ -951,10 +976,7 @@ class Ipd extends BaseController $panelData = $this->ipdModel->getIpdPanelInfo($ipdId); $mode = max(1, min(6, $mode)); - - // Get header parameter from query string (default: show header) - $showHeader = $this->request->getGet('header') !== '0'; - + $data = $this->buildBillDetailsTabData($panelData, $ipdId); $data['show_print_actions'] = false; $data['show_payment_details'] = in_array($mode, [1, 6], true); @@ -973,7 +995,7 @@ class Ipd extends BaseController // When header is hidden (letterhead mode), use larger top margin $marginTop = $showHeader ? 8 : 50; // 50mm = ~5cm for letterhead $marginBottom = $showHeader ? 8 : 20; // 20mm = 2cm - + $mpdf = new Mpdf([ 'tempDir' => $mpdfTempDir, 'format' => 'A4', @@ -989,15 +1011,12 @@ class Ipd extends BaseController $fileName = 'IPD_Bill_' . preg_replace('/[^A-Za-z0-9_\-]/', '_', $ipdCode) . '_M' . $mode . '.pdf'; $pdfBinary = $mpdf->Output($fileName, 'S'); - $this->cacheAbdmIpdBillPdf($ipdId, $pdfBinary); + if (is_string($pdfBinary) && str_starts_with($pdfBinary, '%PDF-')) { + $this->cacheAbdmIpdBillPdf($ipdId, $pdfBinary); + return $pdfBinary; + } - return $this->response - ->setHeader('Content-Type', 'application/pdf') - ->setHeader('Content-Disposition', 'inline; filename="' . $fileName . '"') - ->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') - ->setHeader('Pragma', 'no-cache') - ->setHeader('Expires', '0') - ->setBody($pdfBinary); + return null; } private function cacheAbdmIpdBillPdf(int $ipdId, string $pdfBinary): void diff --git a/app/Controllers/Ipd_discharge.php b/app/Controllers/Ipd_discharge.php index 0bdc446..789e9be 100644 --- a/app/Controllers/Ipd_discharge.php +++ b/app/Controllers/Ipd_discharge.php @@ -8694,6 +8694,235 @@ class Ipd_discharge extends BaseController } } + private function buildIpdPdfDocumentsList(int $ipdId, string $dischargeRaw): array + { + $directory = WRITEPATH . 'uploads' . DIRECTORY_SEPARATOR . 'abdm' . DIRECTORY_SEPARATOR . 'ipd' . DIRECTORY_SEPARATOR . $ipdId; + $createdAt = $this->toIsoDateTimeOrNow($dischargeRaw); + + // Ensure IPD Bill PDF is cached if missing + $billPath = $directory . DIRECTORY_SEPARATOR . 'ipd-bill.pdf'; + if (! is_file($billPath) || filesize($billPath) === 0) { + try { + (new \App\Controllers\Billing\Ipd())->generateIpdBillPdfBinary($ipdId, 1, true); + } catch (\Throwable $e) { + log_message('error', 'Auto-generating IPD Bill PDF failed for IPD {ipd}: {msg}', [ + 'ipd' => $ipdId, + 'msg' => $e->getMessage(), + ]); + } + } + + $documents = []; + $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'], + ]; + + foreach ($definitions as $definition) { + $path = $directory . DIRECTORY_SEPARATOR . $definition['file']; + if (! is_file($path) || filesize($path) === 0) { + continue; + } + $binary = file_get_contents($path); + if (! is_string($binary) || ! str_starts_with($binary, '%PDF-')) { + continue; + } + $documents[] = [ + 'title' => $definition['title'], + 'loinc_code' => $definition['loinc'], + 'snomed_code' => $definition['snomed'], + 'content_type' => 'application/pdf', + 'data' => base64_encode($binary), + 'created_at' => $createdAt, + 'path' => $path, + ]; + } + + return $documents; + } + + private function formatDischargeMedDirections($doseVal, $whenVal, $freqVal, string $days = '', string $remark = ''): string + { + static $whenCodeDescMap = [ + 'BF' => 'BF (BEFORE FOOD)', 'AF' => 'AF (AFTER FOOD)', + 'WF' => 'WF (WITH FOOD)', 'ES' => 'ES (EMPTY STOMACH)', + 'BBF' => 'BBF (BEFORE BREAKFAST)', 'ABF' => 'ABF (AFTER BREAKFAST)', + 'BL' => 'BL (BEFORE LUNCH)', 'AL' => 'AL (AFTER LUNCH)', + 'BD' => 'BD (BEFORE DINNER)', 'AD' => 'AD (AFTER DINNER)', + 'BT' => 'BT (BED TIME)', + ]; + static $whenHindiMap = [ + 'BF' => 'भोजन से पहले', 'BEFORE FOOD' => 'भोजन से पहले', + 'AF' => 'भोजन के बाद', 'AFTER FOOD' => 'भोजन के बाद', + 'WF' => 'भोजन के साथ', 'WITH FOOD' => 'भोजन के साथ', + 'ES' => 'सुबह खाली पेट', 'EMPTY STOMACH' => 'सुबह खाली पेट', + 'BBF'=> 'नाश्ते से पहले', 'BEFORE BREAKFAST' => 'नाश्ते से पहले', + 'ABF'=> 'नाश्ते के बाद', 'AFTER BREAKFAST' => 'नाश्ते के बाद', + 'BL' => 'दोपहर के भोजन से पहले', 'BEFORE LUNCH' => 'दोपहर के भोजन से पहले', + 'AL' => 'दोपहर के भोजन के बाद', 'AFTER LUNCH' => 'दोपहर के भोजन के बाद', + 'BD' => 'रात के भोजन से पहले', 'BEFORE DINNER'=> 'रात के भोजन से पहले', + 'AD' => 'रात के भोजन के बाद', 'AFTER DINNER' => 'रात के भोजन के बाद', + 'BT' => 'रात को सोते समय', 'BED TIME' => 'रात को सोते समय', + ]; + static $freqHindiMap = [ + 'OD' => 'दिन में एक बार (OD)', 'BD' => 'दिन में दो बार (BD)', + 'TDS' => 'दिन में तीन बार (TDS)', 'TID' => 'दिन में तीन बार (TID)', + 'QID' => 'दिन में चार बार (QID)', 'HS' => 'रात को सोते समय (HS)', + 'SOS' => 'ज़रूरत पड़ने पर (SOS)', 'STAT'=> 'तुरंत एक बार (STAT)', + 'ALTERNATE DAY' => 'एक दिन छोड़कर', + 'DAILY' => 'प्रतिदिन', + 'WEEKLY' => 'हफ़्ते में एक बार', + 'MONTHLY' => 'महीने में एक बार', + ]; + static $remarkHindiMap = [ + 'TAKE WITH MILK' => 'दूध के साथ लें', + 'TAKE WITH WARM WATER' => 'गुनगुने पानी के साथ लें', + 'AVOID SOUR FOOD AND DAIRY PRODUCTS'=> 'खट्टा और डेयरी उत्पाद न लें', + 'TAKE AFTER MEALS' => 'भोजन के बाद लें', + 'TAKE ON AN EMPTY STOMACH EARLY MORNING' => 'सुबह खाली पेट लें', + 'CHEW WELL BEFORE SWALLOWING' => 'चबाकर खाएं', + 'DISSOLVE IN HALF GLASS OF WATER' => 'आधे गिलास पानी में घोलकर लें', + 'APPLY LOCALLY TWICE DAILY' => 'दिन में दो बार लगाएं', + 'DO NOT CRUSH OR CHEW TABLET' => 'गोली को तोड़े या चबाएं नहीं', + 'AVOID ALCOHOL WHILE TAKING THIS MEDICINE' => 'शराब का सेवन न करें', + 'COMPLETE FULL COURSE OF ANTIBIOTICS' => 'एंटीबायोटिक का पूरा कोर्स लें', + 'DRINK PLENTY OF FLUIDS / WATER' => 'प्रचुर मात्रा में पानी पिएं', + ]; + + static $doseCache = null; + static $whenCache = null; + static $freqCache = null; + static $whenHindiCache = null; + static $freqHindiCache = null; + + if ($doseCache === null && $this->db->tableExists('opd_dose_shed')) { + $doseCache = []; + foreach ($this->db->table('opd_dose_shed')->select('id, dose_desc')->get()->getResultArray() as $r) { + $doseCache[(int) $r['id']] = trim((string) ($r['dose_desc'] ?? '')); + } + } + if ($whenCache === null && $this->db->tableExists('opd_dose_when')) { + $whenCache = []; + $whenHindiCache = []; + $fields = $this->db->getFieldNames('opd_dose_when') ?? []; + $hasHindi = in_array('dose_sign_hindi', $fields, true); + foreach ($this->db->table('opd_dose_when')->select('id, dose_sign' . ($hasHindi ? ', dose_sign_hindi' : ''))->get()->getResultArray() as $r) { + $whenCache[(int) $r['id']] = trim((string) ($r['dose_sign'] ?? '')); + if ($hasHindi) { + $whenHindiCache[(int) $r['id']] = trim((string) ($r['dose_sign_hindi'] ?? '')); + } + } + } + if ($freqCache === null && $this->db->tableExists('opd_dose_frequency')) { + $freqCache = []; + $freqHindiCache = []; + $fields = $this->db->getFieldNames('opd_dose_frequency') ?? []; + $hasHindi = in_array('dose_sign_hindi', $fields, true); + foreach ($this->db->table('opd_dose_frequency')->select('id, dose_sign' . ($hasHindi ? ', dose_sign_hindi' : ''))->get()->getResultArray() as $r) { + $freqCache[(int) $r['id']] = trim((string) ($r['dose_sign'] ?? '')); + if ($hasHindi) { + $freqHindiCache[(int) $r['id']] = trim((string) ($r['dose_sign_hindi'] ?? '')); + } + } + } + + $doseText = ''; + if (is_numeric($doseVal) && (int) $doseVal > 0) { + $doseText = $doseCache[(int) $doseVal] ?? ''; + } elseif (is_string($doseVal) && trim($doseVal) !== '' && trim($doseVal) !== '0') { + $doseText = trim($doseVal); + } + + $whenText = ''; + $whenHindi = ''; + if (is_numeric($whenVal) && (int) $whenVal > 0) { + $whenText = $whenCache[(int) $whenVal] ?? ''; + $whenHindi = $whenHindiCache[(int) $whenVal] ?? ''; + } elseif (is_string($whenVal) && trim($whenVal) !== '' && trim($whenVal) !== '0') { + $whenText = trim($whenVal); + } + if ($whenText !== '') { + $upperWhen = strtoupper($whenText); + if (isset($whenCodeDescMap[$upperWhen])) { + $whenText = $whenCodeDescMap[$upperWhen]; + } + if ($whenHindi === '' && isset($whenHindiMap[$upperWhen])) { + $whenHindi = $whenHindiMap[$upperWhen]; + } + } + + $freqText = ''; + $freqHindi = ''; + if (is_numeric($freqVal) && (int) $freqVal > 0) { + $freqText = $freqCache[(int) $freqVal] ?? ''; + $freqHindi = $freqHindiCache[(int) $freqVal] ?? ''; + } elseif (is_string($freqVal) && trim($freqVal) !== '' && trim($freqVal) !== '0') { + $freqText = trim($freqVal); + } + if ($freqText !== '') { + $upperFreq = strtoupper($freqText); + if ($freqHindi === '' && isset($freqHindiMap[$upperFreq])) { + $freqHindi = $freqHindiMap[$upperFreq]; + } + } + + $cleanRemark = (string) preg_replace('/^(edit\s*remove|remove\s*edit|edit|remove|delete)\s*$/i', '', trim($remark)); + $cleanRemark = trim($cleanRemark); + $remarkHindi = ''; + if ($cleanRemark !== '') { + $upperRemark = strtoupper($cleanRemark); + if (isset($remarkHindiMap[$upperRemark])) { + $remarkHindi = $remarkHindiMap[$upperRemark]; + } + } + + $dirParts = []; + $dirLocalParts = []; + + if ($whenText !== '') { + $dirParts[] = $whenText; + } + if ($whenHindi !== '') { + $dirLocalParts[] = $whenHindi; + } + + if ($doseText !== '') { + $dirParts[] = $doseText; + } + + if ($freqText !== '') { + $dirParts[] = $freqText; + } + if ($freqHindi !== '') { + $dirLocalParts[] = $freqHindi; + } + + if ($cleanRemark !== '') { + $dirParts[] = $cleanRemark; + } + if ($remarkHindi !== '') { + $dirLocalParts[] = $remarkHindi; + } + + $daysClean = trim($days); + if ($daysClean !== '' && $daysClean !== '0') { + $daysNum = is_numeric($daysClean) ? (int) $daysClean : 0; + $dirParts[] = $daysNum > 0 ? ('for ' . $daysNum . ' days') : ('for ' . $daysClean); + if ($daysNum > 0) { + $dirLocalParts[] = $daysNum . ' दिन'; + } + } + + $engText = implode(' | ', array_values(array_unique(array_filter($dirParts)))); + $hinText = implode(' | ', array_values(array_unique(array_filter($dirLocalParts)))); + + if ($engText !== '' && $hinText !== '') { + return $engText . ' | ' . $hinText; + } + + return $engText !== '' ? $engText : $hinText; + } + public function show_file3(int $ipdId) { return $this->show_discharge($ipdId, 3); @@ -8870,29 +9099,22 @@ class Ipd_discharge extends BaseController } $medicationRows = []; - $legacyMeds = $this->byIpdRows('ipd_discharge_prescrption_prescribed', ['med_name', 'dosage', 'dosage_when', 'dosage_freq', 'no_of_days'], 'id ASC', $ipdId); + $legacyMeds = $this->byIpdRows('ipd_discharge_prescrption_prescribed', ['med_name', 'dosage', 'dosage_when', 'dosage_freq', 'no_of_days', 'remark'], 'id ASC', $ipdId); if (empty($legacyMeds)) { - $legacyMeds = $this->byIpdRows('ipd_discharge_prescription_prescribed', ['med_name', 'dosage', 'dosage_when', 'dosage_freq', 'no_of_days'], 'id ASC', $ipdId); + $legacyMeds = $this->byIpdRows('ipd_discharge_prescription_prescribed', ['med_name', 'dosage', 'dosage_when', 'dosage_freq', 'no_of_days', 'remark'], 'id ASC', $ipdId); } foreach ($legacyMeds as $row) { $name = trim((string) ($row['med_name'] ?? '')); if (! $this->isMeaningfulDischargeValue($name)) { continue; } - $dosageDisplay = $this->buildDosageDisplay( - (int) ($row['dosage'] ?? 0), - (int) ($row['dosage_when'] ?? 0), - (int) ($row['dosage_freq'] ?? 0) + $dose = $this->formatDischargeMedDirections( + $row['dosage'] ?? '', + $row['dosage_when'] ?? '', + $row['dosage_freq'] ?? '', + (string) ($row['no_of_days'] ?? ''), + (string) ($row['remark'] ?? '') ); - $days = trim((string) ($row['no_of_days'] ?? '')); - $doseParts = []; - if ($this->isMeaningfulDischargeValue($dosageDisplay)) { - $doseParts[] = $dosageDisplay; - } - if ($this->isMeaningfulDischargeValue($days)) { - $doseParts[] = 'for ' . $days . ' days'; - } - $dose = implode(' ', $doseParts); $medicationRows[] = ['name' => $name, 'dosage' => $dose]; } if (empty($medicationRows)) { @@ -8909,6 +9131,186 @@ class Ipd_discharge extends BaseController } } + $observationRows = []; + if ($this->tableHasColumns('ipd_discharge_1_b', ['ipd_d_id'])) { + $cols = $this->db->getFieldNames('ipd_discharge_1_b') ?? []; + $sel = in_array('short_head', $cols, true) ? 'short_head,rdata' : '*'; + foreach ($this->db->table('ipd_discharge_1_b')->select($sel)->where('ipd_d_id', $ipdId)->get()->getResultArray() as $row) { + $label = trim((string) ($row['short_head'] ?? '')); + $value = trim((string) ($row['rdata'] ?? '')); + if ($label === '' || ! $this->isMeaningfulDischargeValue($value)) { + continue; + } + $observationRows[] = [ + 'text' => $label, + 'value' => $value, + 'category' => 'Condition on Admission Time', + 'category_code' => 'vital-signs', + 'effective_at' => $admissionIso, + ]; + } + } + if ($this->tableHasColumns('ipd_discharge_1_b_final', ['ipd_d_id'])) { + $cols = $this->db->getFieldNames('ipd_discharge_1_b_final') ?? []; + $sel = in_array('short_head', $cols, true) ? 'short_head,rdata' : '*'; + foreach ($this->db->table('ipd_discharge_1_b_final')->select($sel)->where('ipd_d_id', $ipdId)->get()->getResultArray() as $row) { + $label = trim((string) ($row['short_head'] ?? '')); + $value = trim((string) ($row['rdata'] ?? '')); + if ($label === '' || ! $this->isMeaningfulDischargeValue($value)) { + continue; + } + $observationRows[] = [ + 'text' => $label, + 'value' => $value, + 'category' => 'Condition on Discharge Time', + 'category_code' => 'vital-signs', + 'effective_at' => $dischargeIso, + ]; + } + } + + $investigationRows = []; + if ($this->tableHasColumns('ipd_discharge_1_d', ['ipd_d_id'])) { + $cols = $this->db->getFieldNames('ipd_discharge_1_d') ?? []; + $sel = in_array('short_head', $cols, true) ? 'short_head,rdata' : '*'; + foreach ($this->db->table('ipd_discharge_1_d')->select($sel)->where('ipd_d_id', $ipdId)->get()->getResultArray() as $row) { + $label = trim((string) ($row['short_head'] ?? '')); + $value = trim((string) ($row['rdata'] ?? '')); + if ($label === '' || ! $this->isMeaningfulDischargeValue($value)) { + continue; + } + $investigationRows[] = [ + 'text' => $label . ': ' . $value, + 'authored_on' => $admissionIso, + ]; + } + } + if ($this->tableHasColumns('ipd_discharge_1_e', ['ipd_d_id'])) { + $cols = $this->db->getFieldNames('ipd_discharge_1_e') ?? []; + $sel = in_array('short_head', $cols, true) ? 'short_head,rdata' : '*'; + foreach ($this->db->table('ipd_discharge_1_e')->select($sel)->where('ipd_d_id', $ipdId)->get()->getResultArray() as $row) { + $label = trim((string) ($row['short_head'] ?? '')); + $value = trim((string) ($row['rdata'] ?? '')); + if ($label === '' || ! $this->isMeaningfulDischargeValue($value)) { + continue; + } + $investigationRows[] = [ + 'text' => $label . ': ' . $value, + 'authored_on' => $admissionIso, + ]; + } + } + + $allergyRows = []; + $carePlanRows = []; + $instructionRows = $this->byIpdRows('ipd_discharge_instructions', ['comp_report', 'comp_remark', 'review_after'], 'id DESC', $ipdId); + if (! empty($instructionRows)) { + $instructionRow = $instructionRows[0]; + $instructionMeta = $this->parseInstructionMetaPayload((string) ($instructionRow['comp_report'] ?? '')); + $nabhMeta = is_array($instructionMeta['nabh'] ?? null) ? $instructionMeta['nabh'] : []; + + $allergyStatus = strtolower(trim((string) ($nabhMeta['drug_allergy_status'] ?? ''))); + $allergyDetails = trim((string) ($nabhMeta['drug_allergy_details'] ?? '')); + if ($allergyStatus !== '' || $allergyDetails !== '') { + $allergyRows[] = [ + 'text' => $allergyDetails !== '' ? $allergyDetails : ('Drug allergy status: ' . $allergyStatus), + 'note' => $allergyStatus, + 'criticality' => $allergyStatus === 'yes' ? 'high' : 'low', + 'recorded_at' => $dischargeIso, + ]; + } + + $dischargeAdvice = trim((string) ($instructionRow['comp_remark'] ?? '')); + if ($dischargeAdvice !== '') { + $carePlanRows[] = [ + 'title' => 'Discharge Advice', + 'description' => $dischargeAdvice, + 'created_at' => $dischargeIso, + ]; + } + + $foodIds = is_array($instructionMeta['food_ids'] ?? null) ? ($instructionMeta['food_ids'] ?? []) : []; + if (empty($foodIds) && $this->tableHasColumns('ipd_discharge_drug_food_interaction', ['ipd_id', 'food_id_list'])) { + $legacyFoodRow = $this->firstRowByIpd('ipd_discharge_drug_food_interaction', $ipdId); + $foodIds = $this->parseFoodIdCsv((string) ($legacyFoodRow['food_id_list'] ?? '')); + } + if (! empty($foodIds) && $this->tableHasColumns('ipd_discharge_master_food', ['id', 'food_short', 'food_desc'])) { + $foodRows = $this->db->table('ipd_discharge_master_food') + ->select('id,food_short,food_desc,food_desc_lang') + ->whereIn('id', array_map('intval', $foodIds)) + ->get() + ->getResultArray(); + $foodMap = []; + foreach ($foodRows as $fr) { + $foodMap[(int) $fr['id']] = $fr; + } + $foodLines = []; + $fIndex = 1; + foreach ($foodIds as $fid) { + $fr = $foodMap[(int) $fid] ?? null; + if (! $fr) { + continue; + } + $heading = trim((string) ($fr['food_short'] ?? '')); + $line = trim((string) ($fr['food_desc_lang'] ?? '')); + if ($line === '') { + $line = trim((string) ($fr['food_desc'] ?? '')); + } + if ($heading === '' && $line === '') { + continue; + } + $foodLines[] = $fIndex . '. ' . ($heading !== '' ? $heading . ': ' : '') . $line; + $fIndex++; + } + if (! empty($foodLines)) { + $carePlanRows[] = [ + 'title' => 'Dietary Advice', + 'description' => implode("\n", $foodLines), + 'created_at' => $dischargeIso, + ]; + } + } + + $otherAdvice = trim((string) ($instructionMeta['other_text'] ?? '')); + if ($otherAdvice !== '') { + $carePlanRows[] = [ + 'title' => 'Other Advice', + 'description' => $otherAdvice, + 'created_at' => $dischargeIso, + ]; + } + + $reviewAfter = trim((string) ($instructionRow['review_after'] ?? '')); + if ($reviewAfter !== '') { + $reviewDate = ''; + if ($dischargeRaw !== '' && is_numeric($reviewAfter)) { + $reviewTs = strtotime($dischargeRaw . ' +' . (int) $reviewAfter . ' days'); + if ($reviewTs !== false) { + $reviewDate = ' (' . date('d-m-Y', $reviewTs) . ')'; + } + } + $carePlanRows[] = [ + 'title' => 'Follow Up', + 'description' => 'Review After: ' . $reviewAfter . ' Days' . $reviewDate . ' / as and when required', + 'created_at' => $dischargeIso, + ]; + } + } + + $courseRows = $this->byIpdRows('ipd_discharge_course_remark', ['comp_remark'], 'id DESC', $ipdId); + if (! empty($courseRows)) { + $courseText = trim((string) ($courseRows[0]['comp_remark'] ?? '')); + if ($courseText !== '') { + $carePlanRows[] = [ + 'title' => 'Course In Hospital', + 'description' => $courseText, + 'created_at' => $dischargeIso, + ]; + } + } + + $documentsList = $this->buildIpdPdfDocumentsList($ipdId, $dischargeIso); + $genderRaw = trim((string) ($patientRow['gender'] ?? '')); if ($genderRaw === '') { $genderRaw = trim((string) ($patientRow['xgender'] ?? '')); @@ -8924,6 +9326,7 @@ class Ipd_discharge extends BaseController $source = [ 'record_id' => (string) $ipdId, + 'bundle_identifier' => 'discharge-' . $ipdNo, 'session_id' => (string) $ipdId, 'visit_date' => $visitDate, 'completed_at' => $dischargeIso, @@ -8954,6 +9357,11 @@ class Ipd_discharge extends BaseController 'conditions' => $conditionRows, 'procedures' => $procedureRows, 'medications' => $medicationRows, + 'observations' => $observationRows, + 'investigations' => $investigationRows, + 'allergies' => $allergyRows, + 'care_plans' => $carePlanRows, + 'documents' => $documentsList, 'template' => $this->resolveAbdmDischargeTemplate(), ]; diff --git a/app/Libraries/Abdm/Fhir/Generators/DischargeFhirGenerator.php b/app/Libraries/Abdm/Fhir/Generators/DischargeFhirGenerator.php index 820cd4f..5c16e48 100644 --- a/app/Libraries/Abdm/Fhir/Generators/DischargeFhirGenerator.php +++ b/app/Libraries/Abdm/Fhir/Generators/DischargeFhirGenerator.php @@ -254,10 +254,10 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac continue; } - $stage = trim((string) ($obs['stage'] ?? $obs['timing'] ?? $obs['category_text'] ?? '')); - if ($stage !== '' && stripos($text, 'Admission') === false && stripos($text, 'Discharge') === false) { - $text = $text . ' (' . $stage . ')'; - } + $catText = trim((string) ($obs['category'] ?? $obs['category_text'] ?? $obs['stage'] ?? $obs['timing'] ?? '')); + $isAdm = (stripos($catText, 'Admission') !== false || stripos($text, 'Admission') !== false); + $isDis = (stripos($catText, 'Discharge') !== false || stripos($text, 'Discharge') !== false); + $obsCategoryDisplay = $isAdm ? 'General Examination on Admission' : ($isDis ? 'Examination on Discharge' : 'Vital Signs'); $coding = []; $loincCode = trim((string) ($obs['loinc_code'] ?? '')); @@ -290,7 +290,7 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac 'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Observation']], 'status' => 'final', 'category' => [[ - 'text' => 'Vital Signs' . ($stage !== '' ? ' (' . $stage . ')' : ''), + 'text' => $obsCategoryDisplay, 'coding' => [[ 'system' => 'http://terminology.hl7.org/CodeSystem/observation-category', 'code' => 'vital-signs', @@ -474,6 +474,16 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac continue; } + $docTitle = trim((string) ($document['title'] ?? 'Clinical document')); + $snomedCode = trim((string) ($document['snomed_code'] ?? '')); + if ($snomedCode === '') { + if (stripos($docTitle, 'Bill') !== false || stripos($docTitle, 'Invoice') !== false) { + $snomedCode = '823651000000106'; // Billing record + } else { + $snomedCode = '373942005'; // Discharge summary + } + } + $builder->addDocumentReference([ 'resourceType' => 'DocumentReference', 'id' => 'discharge-document-' . $recordId . '-' . $idx, @@ -482,9 +492,9 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac 'docStatus' => 'final', 'type' => ['coding' => [[ 'system' => 'http://snomed.info/sct', - 'code' => '373942005', - 'display' => 'Discharge summary', - ]], 'text' => 'Discharge Summary'], + 'code' => $snomedCode, + 'display' => $docTitle, + ]], 'text' => $docTitle], 'subject' => ['reference' => $patientRef], 'date' => (string) ($document['created_at'] ?? $timestamp), 'author' => is_array($practitioner) ? [[ @@ -493,35 +503,57 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac 'custodian' => is_array($organization) ? [ 'reference' => 'urn:uuid:' . (string) $organization['id'], ] : null, - 'description' => (string) ($document['title'] ?? 'Clinical document'), + 'description' => $docTitle, 'content' => [[ 'attachment' => [ 'contentType' => (string) ($document['content_type'] ?? 'application/pdf'), 'language' => 'en-IN', 'data' => $data, - 'title' => (string) ($document['title'] ?? 'Clinical document'), + 'title' => $docTitle, 'creation' => (string) ($document['created_at'] ?? $timestamp), ], ]], ]); } + $rawObservations = (array) ($source['observations'] ?? []); + $admissionObsIndexed = []; + $dischargeObsIndexed = []; + foreach ($rawObservations as $idx => $obs) { + $cat = trim((string) ($obs['category'] ?? $obs['category_text'] ?? $obs['stage'] ?? $obs['timing'] ?? '')); + $txt = trim((string) ($obs['text'] ?? '')); + if (stripos($cat, 'Admission') !== false || stripos($txt, 'Admission') !== false) { + $admissionObsIndexed[$idx] = $obs; + } elseif (stripos($cat, 'Discharge') !== false || stripos($txt, 'Discharge') !== false) { + $dischargeObsIndexed[$idx] = $obs; + } + } + $sectionDefinitions = [ ['title' => 'Chief complaints', 'code' => '422843007', 'display' => 'Chief complaint section', 'prefix' => 'discharge-chief-complaint-', 'items' => $chiefComplaintsList, 'narrative' => trim((string) ($source['chief_complaint_narrative'] ?? ''))], ['title' => 'Problems and Diagnoses', 'code' => '1003642006', 'display' => 'Past medical history section', 'prefix' => 'discharge-diagnosis-', 'items' => $diagnosesList, 'narrative' => trim((string) ($source['diagnosis_narrative'] ?? ''))], ['title' => 'Procedures', 'code' => '1003640003', 'display' => 'History of past procedure section', 'prefix' => 'discharge-procedure-', 'items' => (array) ($source['procedures'] ?? [])], ['title' => 'Medications', 'code' => '1003606003', 'display' => 'Medication history section', 'prefix' => 'discharge-medication-', 'items' => (array) ($source['medications'] ?? [])], - ['title' => 'Physical examination', 'code' => '425044008', 'display' => 'Physical exam section', 'prefix' => 'discharge-observation-', 'items' => (array) ($source['observations'] ?? [])], - ['title' => 'Investigations', 'code' => '721981007', 'display' => 'Diagnostic studies report', 'prefix' => 'discharge-investigation-', 'items' => (array) ($source['investigations'] ?? [])], - ['title' => 'Allergies', 'code' => '722446000', 'display' => 'Allergy record', 'prefix' => 'discharge-allergy-', 'items' => (array) ($source['allergies'] ?? [])], - ['title' => 'Care plan', 'code' => '734163000', 'display' => 'Care plan', 'prefix' => 'discharge-careplan-', 'items' => (array) ($source['care_plans'] ?? [])], - ['title' => 'Document reference', 'code' => '373942005', 'display' => 'Discharge summary', 'prefix' => 'discharge-document-', 'items' => (array) ($source['documents'] ?? [])], ]; + + if (! empty($admissionObsIndexed) && ! empty($dischargeObsIndexed)) { + $sectionDefinitions[] = ['title' => 'General Examination on Admission', 'code' => '425044008', 'display' => 'Physical exam section', 'prefix' => 'discharge-observation-', 'items_indexed' => $admissionObsIndexed]; + $sectionDefinitions[] = ['title' => 'Examination on Discharge', 'code' => '425044008', 'display' => 'Physical exam section', 'prefix' => 'discharge-observation-', 'items_indexed' => $dischargeObsIndexed]; + } else { + $sectionDefinitions[] = ['title' => 'Physical examination', 'code' => '425044008', 'display' => 'Physical exam section', 'prefix' => 'discharge-observation-', 'items' => $rawObservations]; + } + + $sectionDefinitions[] = ['title' => 'Investigations', 'code' => '721981007', 'display' => 'Diagnostic studies report', 'prefix' => 'discharge-investigation-', 'items' => (array) ($source['investigations'] ?? [])]; + $sectionDefinitions[] = ['title' => 'Allergies', 'code' => '722446000', 'display' => 'Allergy record', 'prefix' => 'discharge-allergy-', 'items' => (array) ($source['allergies'] ?? [])]; + $sectionDefinitions[] = ['title' => 'Care plan', 'code' => '734163000', 'display' => 'Care plan', 'prefix' => 'discharge-careplan-', 'items' => (array) ($source['care_plans'] ?? [])]; + $sectionDefinitions[] = ['title' => 'Document reference', 'code' => '373942005', 'display' => 'Discharge summary', 'prefix' => 'discharge-document-', 'items' => (array) ($source['documents'] ?? [])]; + $sections = []; foreach ($sectionDefinitions as $definition) { $references = []; $narrativeParts = []; - foreach ($definition['items'] as $idx => $item) { + $items = isset($definition['items_indexed']) ? $definition['items_indexed'] : (array) ($definition['items'] ?? []); + foreach ($items as $idx => $item) { $references[] = ['reference' => 'urn:uuid:' . $definition['prefix'] . $recordId . '-' . $idx]; $narrative = $this->sectionItemNarrative((array) $item); if ($narrative !== '') { @@ -734,13 +766,43 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac /** @param array $item */ private function sectionItemNarrative(array $item): string { + // For medications: name + dosage + if (isset($item['name']) && $this->isMeaningfulValue((string) $item['name'])) { + $name = trim((string) $item['name']); + $dosage = trim((string) ($item['dosage'] ?? '')); + return trim($name . ($this->isMeaningfulValue($dosage) ? ' - ' . $dosage : '')); + } + + // For observations: text + value (+ unit) + if (isset($item['text'], $item['value']) && $this->isMeaningfulValue((string) $item['text']) && $this->isMeaningfulValue((string) $item['value'])) { + $text = trim((string) $item['text']); + $val = trim((string) $item['value']); + $unit = trim((string) ($item['unit'] ?? '')); + $unitInfo = is_numeric($val) ? $this->getVitalSignUnitInfo($text, $unit) : ['unit' => '']; + $displayUnit = $unit !== '' ? $unit : ($unitInfo['unit'] ?? ''); + $valStr = $val . ($displayUnit !== '' && stripos($val, $displayUnit) === false ? ' ' . $displayUnit : ''); + return trim($text . ': ' . $valStr); + } + + // For care plans: title + description + if (isset($item['title'], $item['description']) && $this->isMeaningfulValue((string) $item['description'])) { + $title = trim((string) $item['title']); + $desc = trim((string) $item['description']); + if ($title !== '' && stripos($desc, $title) === false) { + return trim($title . ': ' . strip_tags($desc)); + } + return trim(strip_tags($desc)); + } + + // For documents + if (isset($item['title']) && $this->isMeaningfulValue((string) $item['title'])) { + return trim((string) $item['title']); + } + + // Default fallback foreach (['text', 'description', 'title', 'name', 'value'] as $field) { $value = trim((string) ($item[$field] ?? '')); if ($this->isMeaningfulValue($value)) { - if ($field === 'name') { - $dosage = trim((string) ($item['dosage'] ?? '')); - return trim($value . ($this->isMeaningfulValue($dosage) ? ' - ' . $dosage : '')); - } return trim(strip_tags($value)); } } diff --git a/tests/unit/Abdm/FhirGeneratorsTest.php b/tests/unit/Abdm/FhirGeneratorsTest.php index 82ebd3d..a1acc2d 100644 --- a/tests/unit/Abdm/FhirGeneratorsTest.php +++ b/tests/unit/Abdm/FhirGeneratorsTest.php @@ -207,6 +207,141 @@ final class FhirGeneratorsTest extends CIUnitTestCase $this->assertStringNotContainsString('UNMAPPED', (string) $encoded); } + public function testDischargeRecordWithDualVitalsAdviceAndMultiplePdfs(): void + { + $src = [ + 'record_id' => 3208, + 'bundle_identifier' => 'discharge-A26090003208', + 'session_id' => 3208, + 'visit_date' => '2026-09-04', + 'completed_at' => '2026-09-04T04:58:00+05:30', + 'patient' => [ + 'id' => 15355, + 'uhid' => 'P26091015355', + 'name' => 'JANVI BISHT', + 'gender' => 'female', + 'dob' => '2013-05-10', + 'abha_id' => '91407564383062', + 'abha_address' => 'janvibisht@sbx', + ], + 'encounter' => [ + 'id' => 'A26090003208', + 'class_code' => 'IMP', + 'start' => '2026-09-01T04:53:00+05:30', + 'end' => '2026-09-04T04:58:00+05:30', + ], + 'doctor' => ['id' => 9, 'name' => 'Dr. Sanjay Kumar', 'registration_number' => 'DOC12345'], + 'organization' => ['id' => 'IN0510000000', 'name' => 'DevSoft Tech'], + 'chief_complaints' => [ + ['text' => 'Abdominal Pain'], + ['text' => 'Breathlessness'], + ['text' => 'Cough'], + ['text' => 'Fever'], + ], + 'diagnoses' => [['text' => 'VIRAL FEVER']], + 'procedures' => [['text' => 'Biphosphonate prophylaxis', 'performed_at' => '2026-09-03T00:00:00+05:30']], + 'medications' => [ + [ + 'name' => 'TAB PANTAKOOL DSR', + 'dosage' => 'BF (BEFORE FOOD) | OD | भोजन से पहले | दिन में एक बार (OD)', + ], + [ + 'name' => 'TAB AZIK-500', + 'dosage' => 'AF (AFTER FOOD) | BD | भोजन के बाद | दिन में दो बार (BD)', + ], + ], + 'observations' => [ + ['text' => 'Pulse /min', 'value' => '30/min', 'category' => 'Condition on Admission Time', 'effective_at' => '2026-09-01T04:53:00+05:30'], + ['text' => 'Respiration /min', 'value' => '120/min', 'category' => 'Condition on Admission Time', 'effective_at' => '2026-09-01T04:53:00+05:30'], + ['text' => 'BP mmHg', 'value' => '80mmHg', 'category' => 'Condition on Admission Time', 'effective_at' => '2026-09-01T04:53:00+05:30'], + ['text' => 'Pallor', 'value' => 'Negative', 'category' => 'Condition on Admission Time', 'effective_at' => '2026-09-01T04:53:00+05:30'], + ['text' => 'SPO2', 'value' => '95', 'unit' => '%', 'category' => 'Condition on Admission Time', 'effective_at' => '2026-09-01T04:53:00+05:30'], + ['text' => 'RBS', 'value' => '95', 'unit' => 'mg/dL', 'category' => 'Condition on Admission Time', 'effective_at' => '2026-09-01T04:53:00+05:30'], + ['text' => 'Temp F', 'value' => '99', 'unit' => 'degF', 'category' => 'Condition on Admission Time', 'effective_at' => '2026-09-01T04:53:00+05:30'], + + ['text' => 'Pulse /min', 'value' => '30/min', 'category' => 'Condition on Discharge Time', 'effective_at' => '2026-09-04T04:58:00+05:30'], + ['text' => 'Respiration /min', 'value' => '110/min', 'category' => 'Condition on Discharge Time', 'effective_at' => '2026-09-04T04:58:00+05:30'], + ['text' => 'BP mmHg', 'value' => '90mmHg', 'category' => 'Condition on Discharge Time', 'effective_at' => '2026-09-04T04:58:00+05:30'], + ['text' => 'Pallor', 'value' => 'Negative', 'category' => 'Condition on Discharge Time', 'effective_at' => '2026-09-04T04:58:00+05:30'], + ['text' => 'SPO2', 'value' => '95', 'unit' => '%', 'category' => 'Condition on Discharge Time', 'effective_at' => '2026-09-04T04:58:00+05:30'], + ['text' => 'RBS', 'value' => '105', 'unit' => 'mg/dL', 'category' => 'Condition on Discharge Time', 'effective_at' => '2026-09-04T04:58:00+05:30'], + ['text' => 'Temp F', 'value' => '97', 'unit' => 'degF', 'category' => 'Condition on Discharge Time', 'effective_at' => '2026-09-04T04:58:00+05:30'], + ], + 'investigations' => [ + ['text' => 'Hb %: 11', 'loinc_code' => '718-7'], + ], + 'care_plans' => [ + ['title' => 'Discharge Advice', 'description' => 'To Continue the other medication if any systmic condition like hypertension , diabetic etc, as per adivised by treating physician'], + ['title' => 'Dietary Advice', 'description' => "1. Eat Nutritious diet,Stay away from smoking and alcohol altogether.\n2. Balanced Meals\n3. Fruits and Vegetables\n4. Avoid Processed Foods"], + ['title' => 'Follow Up', 'description' => 'Review After: 5 Days (09-09-2026) / as and when required'], + ], + 'documents' => [ + [ + 'title' => 'IPD Discharge Summary', + 'content_type' => 'application/pdf', + 'data' => base64_encode('%PDF-discharge-summary'), + 'snomed_code' => '373942005', + 'loinc_code' => '18842-5', + 'created_at' => '2026-09-04T04:58:00+05:30', + ], + [ + 'title' => 'IPD Bill / Invoice', + 'content_type' => 'application/pdf', + 'data' => base64_encode('%PDF-ipd-bill'), + 'snomed_code' => '823651000000106', + 'loinc_code' => '75490-3', + 'created_at' => '2026-09-04T04:58:00+05:30', + ], + ], + ]; + + $output = $this->factory->discharge()->generate($src); + $bundle = $output['fhir_bundle']; + $composition = $bundle['entry'][0]['resource']; + + // Verify sections: should contain both General Examination on Admission and Examination on Discharge + $sectionTitles = array_column($composition['section'], 'title'); + $this->assertContains('General Examination on Admission', $sectionTitles); + $this->assertContains('Examination on Discharge', $sectionTitles); + $this->assertContains('Care plan', $sectionTitles); + $this->assertContains('Document reference', $sectionTitles); + + // Verify MedicationRequests have dosageInstruction text + $medRequests = array_values(array_filter($bundle['entry'], static fn (array $e): bool => ($e['resource']['resourceType'] ?? '') === 'MedicationRequest')); + $this->assertCount(2, $medRequests); + $this->assertSame('BF (BEFORE FOOD) | OD | भोजन से पहले | दिन में एक बार (OD)', $medRequests[0]['resource']['dosageInstruction'][0]['text']); + $this->assertSame('AF (AFTER FOOD) | BD | भोजन के बाद | दिन में दो बार (BD)', $medRequests[1]['resource']['dosageInstruction'][0]['text']); + + // Verify CarePlans + $carePlans = array_values(array_filter($bundle['entry'], static fn (array $e): bool => ($e['resource']['resourceType'] ?? '') === 'CarePlan')); + $this->assertCount(3, $carePlans); + $carePlanTitles = array_column(array_column($carePlans, 'resource'), 'title'); + $this->assertContains('Discharge Advice', $carePlanTitles); + $this->assertContains('Dietary Advice', $carePlanTitles); + $this->assertContains('Follow Up', $carePlanTitles); + + // Verify DocumentReferences (both Discharge Summary and Invoice) + $docRefs = array_values(array_filter($bundle['entry'], static fn (array $e): bool => ($e['resource']['resourceType'] ?? '') === 'DocumentReference')); + $this->assertCount(2, $docRefs); + $this->assertSame('IPD Discharge Summary', $docRefs[0]['resource']['description']); + $this->assertSame('IPD Bill / Invoice', $docRefs[1]['resource']['description']); + $this->assertSame('823651000000106', $docRefs[1]['resource']['type']['coding'][0]['code']); + + // Verify Observation Categories on Admission vs Discharge + $observations = array_values(array_filter($bundle['entry'], static fn (array $e): bool => ($e['resource']['resourceType'] ?? '') === 'Observation' && isset($e['resource']['category']))); + $this->assertGreaterThanOrEqual(14, count($observations)); + $this->assertSame('General Examination on Admission', $observations[0]['resource']['category'][0]['text']); + $this->assertSame('Examination on Discharge', $observations[7]['resource']['category'][0]['text']); + + // Verify all references resolve + $fullUrls = array_column($bundle['entry'], 'fullUrl'); + $encoded = json_encode($bundle, JSON_UNESCAPED_SLASHES); + preg_match_all('/"reference":"(urn:uuid:[^"]+)"/', (string) $encoded, $matches); + foreach ($matches[1] as $reference) { + $this->assertContains($reference, $fullUrls, 'Unresolved reference: ' . $reference); + } + } + public function testInvoiceRecordHasNrcesProfilesAndResolvableReferences(): void { $controller = new AbdmGateway();