]*>\s*(\{\{?\s*(?:CLINICAL_SUMMARY|DISCHARGE_SUMMARY)\s*\}?\})\s*<\/h[1-6]>/i',
+ '$1
',
+ $out
+ );
+
+ // Keep only one main summary placeholder; extra occurrences cause duplicate output.
+ $seenSummary = false;
+ $out = (string) preg_replace_callback(
+ '/\{\{?\s*(CLINICAL_SUMMARY|DISCHARGE_SUMMARY)\s*\}?\}/i',
+ static function (array $m) use (&$seenSummary): string {
+ if ($seenSummary) {
+ return '';
+ }
+
+ $seenSummary = true;
+ return '{{DISCHARGE_SUMMARY}}';
+ },
+ $out
+ );
+
+ return $out;
+ }
+
private function applyDischargeTemplate(string $content, array $panelData, ?int $requestedTemplateId = null): array
{
$templates = $this->getDischargeTemplateRows();
@@ -746,7 +862,19 @@ class Ipd_discharge extends BaseController
$templateHtml = '{{CONTENT}}';
}
+ // Normalize template syntax early so downstream placeholder checks operate
+ // on a consistent token format (legacy PHP placeholders, casing, spacing).
+ $templateHtml = $this->normalizeLegacyDischargeTemplate($templateHtml);
+ $templateHtml = $this->normalizeDischargeTemplatePlaceholders($templateHtml);
+
$tokenVars = $this->buildDischargeTemplateTokenVars($panelData, $content);
+
+ $templateHasMainSummaryToken = preg_match('/\{\{\s*DISCHARGE_SUMMARY\s*\}\}|\{\s*DISCHARGE_SUMMARY\s*\}/i', $templateHtml) === 1;
+ if ($templateHasMainSummaryToken) {
+ $tokenVars['DISCHARGE_ADVICE'] = '';
+ $tokenVars['INSTRUCTION_REMARK'] = '';
+ }
+
if (! $this->templateHasKnownDischargeTokens($templateHtml, $tokenVars)) {
$templateHtml .= "\n{{CONTENT}}";
}
@@ -758,6 +886,11 @@ class Ipd_discharge extends BaseController
$templateHtml = '{{CONTENT}}';
}
+ // If template is section-driven, ignore raw CONTENT token to avoid duplicate blocks.
+ if ($this->templateHasDischargeSectionPlaceholders($templateHtml)) {
+ $templateHtml = (string) preg_replace('/\{\{?\s*content\s*\}?\}/i', '', $templateHtml);
+ }
+
$content = $this->stripLegacyTopSummaryFromContent($content, $templateHtml, $patientName, $patientCode, $ipdCode);
$templateSettings = array_merge($this->defaultDischargeTemplateSettings(), [
@@ -781,6 +914,7 @@ class Ipd_discharge extends BaseController
}
$rendered = $this->applyDischargeTemplateTokens($templateHtml, $tokenVars);
+ $rendered = $this->dedupeRepeatedClinicalSummary($rendered, (string) ($tokenVars['DISCHARGE_SUMMARY'] ?? ''));
return [
'rendered_html' => $rendered,
@@ -791,6 +925,33 @@ class Ipd_discharge extends BaseController
];
}
+ private function dedupeRepeatedClinicalSummary(string $renderedHtml, string $clinicalSummaryHtml): string
+ {
+ $renderedHtml = (string) $renderedHtml;
+ $clinicalSummaryHtml = trim((string) $clinicalSummaryHtml);
+ if ($renderedHtml === '' || $clinicalSummaryHtml === '') {
+ return $renderedHtml;
+ }
+
+ $firstPos = strpos($renderedHtml, $clinicalSummaryHtml);
+ if ($firstPos === false) {
+ return $renderedHtml;
+ }
+
+ $offset = $firstPos + strlen($clinicalSummaryHtml);
+ while (true) {
+ $nextPos = strpos($renderedHtml, $clinicalSummaryHtml, $offset);
+ if ($nextPos === false) {
+ break;
+ }
+
+ $renderedHtml = substr_replace($renderedHtml, '', $nextPos, strlen($clinicalSummaryHtml));
+ $offset = $nextPos;
+ }
+
+ return $renderedHtml;
+ }
+
private function templateHasKnownDischargeTokens(string $templateHtml, array $tokenVars): bool
{
if (trim($templateHtml) === '') {
@@ -853,7 +1014,9 @@ class Ipd_discharge extends BaseController
}
$sectionTokens = [
+ 'CLINICAL_SUMMARY',
'DISCHARGE_SUMMARY',
+ 'FOLLOW_UP_INSTRUCTIONS',
'FINAL_DIAGNOSIS',
'SURGERY',
'PROCEDURE',
@@ -868,6 +1031,8 @@ class Ipd_discharge extends BaseController
'CO_MORBIDITIES',
'DISCHARGE_MEDICATIONS',
'DIETARY_ADVICE',
+ 'OTHER_ADVICE',
+ 'REVIEW_AFTER',
'DISCHARGE_INSTRUCTIONS',
'SIGNATURE_BLOCK',
];
@@ -1073,19 +1238,25 @@ class Ipd_discharge extends BaseController
return 'Discharge Summary';
}
- $ipdStatus = (int) ($ipd->ipd_status ?? 0);
+ $ipdStatus = (int) ($ipd->discarge_patient_status ?? ($ipd->discharge_status ?? ($ipd->ipd_status ?? 0)));
// If status is 0 (Status Pending), show "Discharge Summary" only
if ($ipdStatus === 0) {
return 'Discharge Summary';
}
- // Check if ipd_discharge_status table exists
- if (! $this->db->tableExists('ipd_discharge_status')) {
+ $statusTable = '';
+ if ($this->db->tableExists('ipd_discharg_status')) {
+ $statusTable = 'ipd_discharg_status';
+ } elseif ($this->db->tableExists('ipd_discharge_status')) {
+ $statusTable = 'ipd_discharge_status';
+ }
+
+ if ($statusTable === '') {
return 'Discharge Summary';
}
- $row = $this->db->table('ipd_discharge_status')
+ $row = $this->db->table($statusTable)
->select('status_desc, status_details')
->where('id', $ipdStatus)
->get(1)
@@ -4902,6 +5073,172 @@ class Ipd_discharge extends BaseController
);
}
+ /**
+ * Placeholder preview page: shows each placeholder and resolved content for an IPD.
+ * Access via: /Ipd_discharge/placeholder_preview/{ipdId}?tpl={templateId}
+ */
+ public function placeholder_preview(int $ipdId = 0)
+ {
+ $permission = $this->requireAnyPermission([
+ 'billing.access',
+ 'billing.ipd.invoice',
+ 'billing.ipd.current-admission',
+ 'template.discharge',
+ ]);
+ if ($permission) {
+ return $permission;
+ }
+
+ if ($ipdId <= 0) {
+ $ipdId = (int) ($this->request->getGet('ipd_id') ?? 0);
+ }
+ $requestedTemplateId = (int) ($this->request->getGet('tpl') ?? 0);
+ $forceRegenerate = (int) ($this->request->getGet('refresh') ?? 0) === 1;
+
+ $placeholderInfo = [
+ 'H_address_1' => 'Hospital Address Line 1',
+ 'H_address_2' => 'Hospital Address Line 2',
+ 'H_logo' => 'Hospital Logo File Name',
+ 'H_logo_abs' => 'Hospital Logo Absolute Path',
+ 'hospital_name' => 'Hospital Name',
+ 'hospital_address' => 'Hospital Full Address',
+ 'hospital_phone' => 'Hospital Phone',
+ 'hospital_email' => 'Hospital Email',
+ 'PATIENT_NAME' => 'Patient Name',
+ 'UHID' => 'Patient UHID',
+ 'IPD_CODE' => 'IPD Number/Code',
+ 'AGE_GENDER' => 'Age / Gender',
+ 'GUARDIAN' => 'Guardian Combined Text',
+ 'GUARDIAN_RELATION' => 'Guardian Relation',
+ 'GUARDIAN_NAME' => 'Guardian Name',
+ 'PATIENT_ADDRESS' => 'Patient Address',
+ 'PATIENT_PHONE' => 'Patient Phone',
+ 'DEPARTMENT' => 'Department Name',
+ 'ADMIT_DATE' => 'Admission Date',
+ 'DISCHARGE_DATE' => 'Discharge Date',
+ 'ADMISSION_TIME' => 'Admission Time',
+ 'DISCHARGE_TIME' => 'Discharge Time',
+ 'ISDELIVERY' => 'Delivery Flag',
+ 'INSURANCE_COMPANY' => 'Insurance Company',
+ 'DOCTOR_NAMES' => 'Consultant/Doctor Names',
+ 'DOCTOR_NAME' => 'Doctor Name (alias)',
+ 'CURRENT_DATE' => 'Current Date',
+ 'PRINT_TIME' => 'Print Date/Time',
+ 'CONTENT' => 'Full Auto-Generated Discharge Content',
+ 'DISCHARGE_STATUS' => 'Document Heading from Discharge Status',
+ 'PATIENT_INFO_TABLE' => 'Pre-built Patient Information Table',
+ 'DISCHARGE_SUMMARY' => 'Main Discharge Summary Content',
+ 'FINAL_DIAGNOSIS' => 'Final Diagnosis Section',
+ 'SURGERY' => 'Surgery Section',
+ 'PROCEDURE' => 'Procedure Section',
+ 'PERSONAL_HISTORY' => 'Personal History Section',
+ 'PRESENTING_COMPLAINTS' => 'Presenting Complaints Section',
+ 'PAIN_MEASUREMENT_SCALE' => 'Pain Measurement Section',
+ 'GENERAL_EXAM_ADMISSION' => 'General Examination on Admission',
+ 'CLINICAL_INVESTIGATION_REPORTS' => 'Clinical Investigation Reports',
+ 'COURSE_IN_HOSPITAL' => 'Course in Hospital Section',
+ 'EXAMINATION_ON_DISCHARGE' => 'Examination on Discharge Section',
+ 'DRUG_ALLERGY_ADR' => 'Drug Allergy/ADR Section',
+ 'CO_MORBIDITIES' => 'Co-Morbidities Section',
+ 'DISCHARGE_MEDICATIONS' => 'Discharge Medications Section',
+ 'DIETARY_ADVICE' => 'Dietary Advice Section',
+ 'OTHER_ADVICE' => 'Other Advice Section',
+ 'REVIEW_AFTER' => 'Review After (days/text)',
+ 'FOLLOW_UP_INSTRUCTIONS' => 'Follow-up Instructions Section',
+ 'DISCHARGE_ADVICE' => 'Follow-up Advice (legacy alias)',
+ 'INSTRUCTION_REMARK' => 'Instruction Remark (legacy alias)',
+ 'SIGNATURE_BLOCK' => 'Signature Block',
+ ];
+
+ if ($ipdId <= 0) {
+ $urlBase = site_url('Ipd_discharge/placeholder_preview');
+ return $this->response
+ ->setContentType('text/html')
+ ->setBody(
+ 'Discharge Placeholder Preview ' .
+ '' .
+ 'Discharge Placeholder Preview ' .
+ '
Use this page to see which placeholder resolves to what content for a given IPD.
' .
+ '
'
+ );
+ }
+
+ $panelData = $this->ipdBillingModel->getIpdPanelInfo($ipdId);
+ if (empty($panelData)) {
+ return $this->response->setStatusCode(404)->setBody('IPD not found');
+ }
+
+ $content = $this->getDischargeContent($ipdId);
+ if ($forceRegenerate || trim(strip_tags($content)) === '') {
+ $content = $this->buildAutoDischargeContent($ipdId, $panelData);
+ if (trim(strip_tags($content)) !== '') {
+ $this->saveDischargeContent($ipdId, $content);
+ }
+ }
+
+ $templatePack = $this->applyDischargeTemplate($content, $panelData, $requestedTemplateId > 0 ? $requestedTemplateId : null);
+ $tokenVars = $this->buildDischargeTemplateTokenVars($panelData, $content);
+ $selectedTemplateId = (int) ($templatePack['selected_template_id'] ?? 0);
+ $selectedTemplateName = (string) ($templatePack['selected_template_name'] ?? '');
+ $showLegacy = (int) ($this->request->getGet('show_legacy') ?? 0) === 1;
+ $legacyTokens = [
+ 'FOLLOW_UP_INSTRUCTIONS',
+ 'DISCHARGE_ADVICE',
+ 'INSTRUCTION_REMARK',
+ ];
+ $legacyTokenSet = array_fill_keys($legacyTokens, true);
+
+ $rowsHtml = '';
+ foreach ($placeholderInfo as $token => $description) {
+ $value = (string) ($tokenVars[$token] ?? '');
+ $plain = trim(strip_tags($value));
+ $status = $plain === '' ? 'Empty' : 'Filled';
+ $statusColor = $plain === '' ? '#b45309' : '#166534';
+ $display = $value === '' ? '(empty) ' : '' . htmlspecialchars($value, ENT_QUOTES, 'UTF-8') . ' ';
+ $isLegacyToken = isset($legacyTokenSet[$token]);
+ $rowClassAttr = $isLegacyToken ? ' class="legacy-row"' : '';
+ $rowStyleAttr = ($isLegacyToken && !$showLegacy) ? ' style="display:none;"' : '';
+
+ $rowsHtml .= ''
+ . '{{' . esc($token) . '}} '
+ . '' . esc($description) . ' '
+ . '' . $status . ' '
+ . '' . $display . ' '
+ . ' ';
+ }
+
+ $previewUrl = site_url('Ipd_discharge/preview_discharge_report/' . $ipdId) . ($selectedTemplateId > 0 ? ('?tpl=' . $selectedTemplateId) : '');
+ $pdfUrl = site_url('Ipd_discharge/show_discharge/' . $ipdId . '/1') . ($selectedTemplateId > 0 ? ('?tpl=' . $selectedTemplateId) : '');
+
+ return $this->response
+ ->setContentType('text/html')
+ ->setBody(
+ 'Discharge Placeholder Map - IPD ' . $ipdId . ' ' .
+ '' .
+ '' .
+ '' .
+ '
Discharge Placeholder Mapping ' .
+ '
IPD ID: ' . $ipdId . ' Template: ' . esc($selectedTemplateName) . ' (ID: ' . $selectedTemplateId . ')
' .
+ '
' .
+ '
Show legacy placeholders (' . count($legacyTokens) . ' hidden by default)
' .
+ '
This table shows each placeholder and resolved value from current discharge data.
' .
+ '
' .
+ 'Placeholder Content Source Status Resolved Content '
+ . $rowsHtml .
+ '
' .
+ '' .
+ ''
+ );
+ }
+
public function ipd_select(int $ipdId, int $reCreate = 0)
{
$permission = $this->requireAnyPermission([
@@ -5904,6 +6241,20 @@ class Ipd_discharge extends BaseController
}
if ($savedAny) {
+ // Keep ipd_discharge.content in sync with latest form data so preview/PDF
+ // immediately reflects edits without requiring manual regen links.
+ try {
+ $freshPanelData = $this->ipdBillingModel->getIpdPanelInfo($ipdId);
+ if (! empty($freshPanelData)) {
+ $freshContent = $this->buildAutoDischargeContent($ipdId, $freshPanelData);
+ if (trim((string) $freshContent) !== '') {
+ $this->saveDischargeContent($ipdId, (string) $freshContent);
+ }
+ }
+ } catch (\Throwable $e) {
+ // Fail-open: do not block UI save if regeneration fails.
+ }
+
$this->enqueueIpdDischargeSync($ipdId, $patientId, $action);
}
diff --git a/app/Views/Setting/Template/discharge_templates.php b/app/Views/Setting/Template/discharge_templates.php
index 7aef3bc..96fc402 100644
--- a/app/Views/Setting/Template/discharge_templates.php
+++ b/app/Views/Setting/Template/discharge_templates.php
@@ -36,13 +36,13 @@ $status = (int) ($edit['status'] ?? 1);
Available placeholders:
- 1. Hospital Information: {{H_Name}}, {{H_address_1}}, {{H_address_2}}, {{H_phone_No}}, {{H_Email}}, {{H_logo}}, {{H_logo_abs}}, {{hospital_name}}, {{hospital_address}}, {{hospital_phone}}, {{hospital_email}}
+ 1. Hospital Information: {{hospital_name}}, {{H_address_1}}, {{H_address_2}}, {{hospital_phone}}, {{hospital_email}}, {{H_logo}}, {{H_logo_abs}}, {{hospital_address}}
2. Patient Information: {{PATIENT_NAME}}, {{UHID}}, {{IPD_CODE}}, {{AGE_GENDER}}, {{GUARDIAN}}, {{GUARDIAN_RELATION}}, {{GUARDIAN_NAME}}, {{PATIENT_ADDRESS}}, {{PATIENT_PHONE}} ✨
- 3. IPD Information: {{DEPARTMENT}}, {{ADMIT_DATE}}, {{DISCHARGE_DATE}}, {{ADMIT_DATE_ONLY}}, {{DISCHARGE_DATE_ONLY}}, {{ADMISSION_TIME}}, {{DISCHARGE_TIME}}, {{ADMIT_TIME}}, {{ISDELIVERY}}, {{INSURANCE_COMPANY}} ✨, {{DOCTOR_NAMES}} ✨, {{DOCTOR_NAME}} ✨
+ 3. IPD Information: {{DEPARTMENT}}, {{ADMIT_DATE}}, {{DISCHARGE_DATE}}, {{ADMISSION_TIME}}, {{DISCHARGE_TIME}}, {{ISDELIVERY}}, {{INSURANCE_COMPANY}} ✨, {{DOCTOR_NAMES}} ✨, {{DOCTOR_NAME}} ✨
4. IPD Discharge Content: {{CONTENT}} (all sections), {{PATIENT_INFO_TABLE}} (pre-built patient table)
- Section placeholders for custom layout/order: {{DISCHARGE_SUMMARY}}, {{FINAL_DIAGNOSIS}}, {{SURGERY}}, {{PROCEDURE}}, {{PERSONAL_HISTORY}}, {{PRESENTING_COMPLAINTS}}, {{PAIN_MEASUREMENT_SCALE}}, {{GENERAL_EXAM_ADMISSION}}, {{CLINICAL_INVESTIGATION_REPORTS}}, {{COURSE_IN_HOSPITAL}}, {{EXAMINATION_ON_DISCHARGE}}, {{DRUG_ALLERGY_ADR}}, {{CO_MORBIDITIES}}, {{DISCHARGE_MEDICATIONS}}, {{DIETARY_ADVICE}}, {{DISCHARGE_INSTRUCTIONS}}, {{SIGNATURE_BLOCK}}
- 5. Common/Meta: {{CURRENT_DATE}}, {{PRINT_TIME}}
- ✨ = Newly added tokens | All tokens are case-insensitive | Empty values auto-hide
+ Section placeholders for custom layout/order: {{DISCHARGE_SUMMARY}}, {{FINAL_DIAGNOSIS}}, {{SURGERY}}, {{PROCEDURE}}, {{PERSONAL_HISTORY}}, {{PRESENTING_COMPLAINTS}}, {{PAIN_MEASUREMENT_SCALE}}, {{GENERAL_EXAM_ADMISSION}}, {{CLINICAL_INVESTIGATION_REPORTS}}, {{COURSE_IN_HOSPITAL}}, {{EXAMINATION_ON_DISCHARGE}}, {{DRUG_ALLERGY_ADR}}, {{CO_MORBIDITIES}}, {{DISCHARGE_MEDICATIONS}}, {{DIETARY_ADVICE}}, {{OTHER_ADVICE}}, {{REVIEW_AFTER}}, {{FOLLOW_UP_INSTRUCTIONS}}, {{SIGNATURE_BLOCK}}
+ 5. Common/Meta: {{DISCHARGE_STATUS}}, {{CURRENT_DATE}}, {{PRINT_TIME}}
+ ✨ = Newly added tokens | All tokens are case-insensitive | Empty values auto-hide | Legacy aliases auto-normalized to preferred placeholders
@@ -64,10 +64,11 @@ $status = (int) ($edit['status'] ?? 1);
Preview Discharge
Open PDF Print
+ Placeholder Data Preview
- Uses the live discharge routes: /Ipd_discharge/preview_discharge_report/{ipdId} and /Ipd_discharge/show_discharge/{ipdId}/1.
+ Uses the live discharge routes: /Ipd_discharge/preview_discharge_report/{ipdId}, /Ipd_discharge/show_discharge/{ipdId}/1, and /Ipd_discharge/placeholder_preview/{ipdId}.
@@ -224,6 +225,7 @@ $status = (int) ($edit['status'] ?? 1);
var previewIpdInput = document.getElementById('discharge_preview_ipd_id');
var previewBtn = document.getElementById('btn_discharge_preview');
var pdfBtn = document.getElementById('btn_discharge_pdf');
+ var placeholderPreviewBtn = document.getElementById('btn_discharge_placeholder_preview');
var editButtons = document.querySelectorAll('.discharge-template-edit');
var deleteButtons = document.querySelectorAll('.discharge-template-delete');
@@ -674,6 +676,13 @@ $status = (int) ($edit['status'] ?? 1);
});
}
+ if (placeholderPreviewBtn) {
+ placeholderPreviewBtn.addEventListener('click', function () {
+ var ipdId = parseInt((previewIpdInput && previewIpdInput.value) ? previewIpdInput.value : '0', 10);
+ openDischargeUrl('= site_url('Ipd_discharge/placeholder_preview') ?>', ipdId);
+ });
+ }
+
// Handle "Create Default Templates" button
var createDefaultsBtn = document.getElementById('btn_create_defaults');
if (createDefaultsBtn) {