Enhance IPD discharge summary FHIR bundle: dual admission/discharge vitals, full medication dosage directions, dietary/discharge advice care plans, and IPD invoice PDF attachment

This commit is contained in:
DevSoftTech Server 2026-09-04 05:39:45 +05:30
parent 4058850941
commit 4776bfb37b
5 changed files with 928 additions and 86 deletions

View File

@ -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

View File

@ -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

View File

@ -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(),
];

View File

@ -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<string,mixed> $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));
}
}

View File

@ -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();