Add IPD discharge template builder

This commit is contained in:
DevSoftTech Server 2026-08-18 15:18:13 +05:30
parent e1de6e3215
commit 1fec6c8677
5 changed files with 505 additions and 1 deletions

View File

@ -1024,6 +1024,8 @@ $routes->match(['GET', 'POST'], 'setting/template/discharge_templates', 'Setting
$routes->post('setting/template/discharge_templates_seed', 'Setting\Template::discharge_templates_seed');
$routes->get('setting/template/discharge_template_get/(:num)', 'Setting\Template::discharge_template_get/$1');
$routes->match(['GET', 'POST'], 'setting/template/discharge_templates/delete/(:num)', 'Setting\Template::discharge_template_delete/$1');
$routes->get('Ipd_discharge/print_template_builder', 'Ipd_discharge::print_template_builder', ['filter' => 'permission:template.discharge,ipd_discharge.manage']);
$routes->post('Ipd_discharge/discharge_template_rename_ajax', 'Ipd_discharge::discharge_template_rename_ajax', ['filter' => 'permission:template.discharge,ipd_discharge.manage']);
$routes->match(['GET', 'POST'], 'setting/template/ipd_document_templates', 'Setting\Template::ipd_document_templates');
$routes->get('setting/template/ipd_document_templates/delete/(:num)', 'Setting\Template::ipd_document_template_delete/$1');
$routes->match(['GET', 'POST'], 'setting/template/diagnosis_print_settings', 'Setting\Template::diagnosis_print_settings');

View File

@ -5286,6 +5286,64 @@ class Ipd_discharge extends BaseController
]);
}
public function print_template_builder()
{
if ($resp = $this->requireAnyPermission(['template.discharge'])) {
return $resp;
}
$mode = strtolower(trim((string) $this->request->getGet('mode')));
if (! in_array($mode, ['list', 'edit'], true)) {
$mode = 'list';
}
$this->ensureDischargeTemplateTable();
$rows = $this->db->table('ipd_discharge_templates')
->orderBy('is_default', 'DESC')
->orderBy('id', 'ASC')
->get()
->getResultArray();
if ($mode === 'list') {
return view('ipd_discharge/discharge_template_list', ['rows' => $rows]);
}
$editId = (int) ($this->request->getGet('edit') ?? 0);
$editRow = [];
if ($editId > 0) {
foreach ($rows as $row) {
if ((int) ($row['id'] ?? 0) === $editId) {
$editRow = $row;
break;
}
}
}
return view('ipd_discharge/discharge_template_edit', [
'rows' => $rows,
'edit_row' => $editRow,
]);
}
public function discharge_template_rename_ajax()
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['update' => 0]);
}
if ($resp = $this->requireAnyPermission(['template.discharge'])) {
return $this->response->setStatusCode(403)->setJSON(['update' => 0]);
}
$id = (int) ($this->request->getPost('id') ?? 0);
$name = trim((string) ($this->request->getPost('template_name') ?? ''));
if ($id <= 0 || $name === '') {
return $this->response->setJSON(['update' => 0, 'error_text' => 'ID and name required.',
'csrfName' => csrf_token(), 'csrfHash' => csrf_hash()]);
}
$ok = (bool) $this->db->table('ipd_discharge_templates')->where('id', $id)->update(['template_name' => $name]);
return $this->response->setJSON(['update' => $ok ? 1 : 0, 'error_text' => $ok ? 'Renamed.' : 'Unable to rename.',
'csrfName' => csrf_token(), 'csrfHash' => csrf_hash()]);
}
public function search_patient_ajax()
{
$permission = $this->requireAnyPermission([

View File

@ -211,7 +211,7 @@ $hasTemplateAccess = $canPathology || $canUltrasound || $canXray || $canCt || $c
<?php if ($canDischarge) { ?>
<div class="col-6 col-md-2 col-lg-2">
<a class="admin-tile" href="javascript:load_form_div('<?= base_url('setting/template/discharge_templates') ?>','maindiv','IPD Discharge Template');">
<a class="admin-tile" href="javascript:load_form('<?= base_url('Ipd_discharge/print_template_builder') ?>?mode=list','IPD Discharge Template');">
<span class="tile-icon"><i class="bi bi-file-earmark-medical"></i></span>
<span>IPD Discharge Template</span>
</a>

View File

@ -0,0 +1,292 @@
<?php
$rows = $rows ?? [];
$edit = $edit_row ?? [];
$editId = (int) ($edit['id'] ?? 0);
$tmplName = (string) ($edit['template_name'] ?? '');
$headerHtml = (string) ($edit['header_html'] ?? '');
$footerHtml = (string) ($edit['footer_html'] ?? '');
$templateCss = (string) ($edit['template_css'] ?? '');
$templateHtml= (string) ($edit['template_html'] ?? '{{CONTENT}}');
$pageSize = strtoupper((string) ($edit['page_size'] ?? 'A4'));
$marginTop = (string) ($edit['page_margin_top_cm'] ?? '3.00');
$marginBottom= (string) ($edit['page_margin_bottom_cm'] ?? '0.50');
$marginLeft = (string) ($edit['page_margin_left_cm'] ?? '0.80');
$marginRight = (string) ($edit['page_margin_right_cm'] ?? '0.80');
$marginHeader= (string) ($edit['margin_header_cm'] ?? '1.00');
$marginFooter= (string) ($edit['margin_footer_cm'] ?? '0.50');
$isDefault = (int) ($edit['is_default'] ?? 0);
$isAuditOnly = (int) ($edit['is_audit_only'] ?? 0);
$status = (int) ($edit['status'] ?? 1);
$placeholders = [
'Hospital' => [
'H_Name' => 'Hospital Name', 'hospital_address' => 'Hospital Address',
'H_phone_No' => 'Hospital Phone', 'H_Email' => 'Hospital Email',
'H_logo' => 'Hospital Logo (filename)', 'H_logo_abs' => 'Hospital Logo (absolute path for mPDF)',
],
'Patient' => [
'PATIENT_NAME' => 'Patient Name', 'UHID' => 'UHID', 'AGE_GENDER' => 'Age / Gender',
'GUARDIAN' => 'Guardian', 'PATIENT_ADDRESS' => 'Address', 'PATIENT_PHONE' => 'Phone',
'INSURANCE_COMPANY' => 'TPA/Insurance', 'DEPARTMENT' => 'Department', 'DOCTOR_NAMES' => 'Doctors',
],
'IPD Info' => [
'IPD_CODE' => 'IPD No.', 'ADMIT_DATE' => 'Admission Date', 'ADMISSION_TIME' => 'Admission Time',
'DISCHARGE_DATE' => 'Discharge Date', 'DISCHARGE_TIME' => 'Discharge Time',
'DISCHARGE_STATUS' => 'Discharge Status Heading',
],
'Clinical Content' => [
'CONTENT' => 'Full discharge content (all sections)',
'FINAL_DIAGNOSIS' => 'Final Diagnosis', 'SURGERY' => 'Surgery', 'PROCEDURE' => 'Procedure',
'PRESENTING_COMPLAINTS' => 'Presenting Complaints',
'GENERAL_EXAM_ADMISSION' => 'General Exam on Admission',
'CLINICAL_INVESTIGATION_REPORTS' => 'Investigation Reports',
'COURSE_IN_HOSPITAL' => 'Course in Hospital',
'EXAMINATION_ON_DISCHARGE' => 'Exam on Discharge',
'DISCHARGE_MEDICATIONS' => 'Discharge Medications',
'DISCHARGE_SUMMARY' => 'Summary/Advice', 'REVIEW_AFTER' => 'Review After',
'DIETARY_ADVICE' => 'Dietary Advice',
'DRUG_ALLERGY_ADR' => 'Drug Allergy/ADR', 'CO_MORBIDITIES' => 'Co-Morbidities',
'PERSONAL_HISTORY' => 'Personal History',
'PATIENT_INFO_TABLE' => 'Pre-built Patient Info Table',
],
'Print Meta' => [
'CURRENT_DATE' => 'Print Date', 'PRINT_TIME' => 'Print Date/Time',
],
];
?>
<section class="content-header">
<div class="d-flex justify-content-between align-items-center">
<h1 class="mb-0">IPD Discharge Template Builder</h1>
<a class="btn btn-outline-secondary btn-sm" href="javascript:load_form('<?= base_url('Ipd_discharge/print_template_builder') ?>?mode=list','IPD Discharge Templates');">
← Template List
</a>
</div>
</section>
<section class="content mt-3">
<?= csrf_field() ?>
<input type="hidden" id="tpl_edit_id" value="<?= $editId ?>">
<div id="tpl_edit_msg" class="mb-2"></div>
<div class="row">
<!-- Left: Editor -->
<div class="col-md-6">
<div class="card mb-3">
<div class="card-header py-2"><strong><?= $editId > 0 ? 'Edit Template' : 'New Template' ?></strong></div>
<div class="card-body">
<div class="row g-2 mb-2">
<div class="col-8">
<label class="form-label form-label-sm">Template Name <span class="text-danger">*</span></label>
<input type="text" class="form-control form-control-sm" id="tpl_name" value="<?= esc($tmplName) ?>">
</div>
<div class="col-4">
<label class="form-label form-label-sm">Status</label>
<select class="form-select form-select-sm" id="tpl_status">
<option value="1" <?= $status === 1 ? 'selected' : '' ?>>Active</option>
<option value="0" <?= $status === 0 ? 'selected' : '' ?>>Inactive</option>
</select>
</div>
</div>
<div class="row g-2 mb-2">
<div class="col-4">
<label class="form-label form-label-sm">Page Size</label>
<select class="form-select form-select-sm" id="tpl_page_size">
<?php foreach (['A4','A4-L','A5','A6','LETTER','LEGAL'] as $ps): ?>
<option value="<?= $ps ?>" <?= $pageSize === $ps ? 'selected' : '' ?>><?= $ps ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-4">
<label class="form-label form-label-sm">Top (cm)</label>
<input type="number" class="form-control form-control-sm" id="tpl_margin_top" step="0.1" min="0" max="25" value="<?= esc($marginTop) ?>">
</div>
<div class="col-4">
<label class="form-label form-label-sm">Bottom (cm)</label>
<input type="number" class="form-control form-control-sm" id="tpl_margin_bottom" step="0.1" min="0" max="25" value="<?= esc($marginBottom) ?>">
</div>
<div class="col-4">
<label class="form-label form-label-sm">Left (cm)</label>
<input type="number" class="form-control form-control-sm" id="tpl_margin_left" step="0.1" min="0" max="25" value="<?= esc($marginLeft) ?>">
</div>
<div class="col-4">
<label class="form-label form-label-sm">Right (cm)</label>
<input type="number" class="form-control form-control-sm" id="tpl_margin_right" step="0.1" min="0" max="25" value="<?= esc($marginRight) ?>">
</div>
<div class="col-4">
<label class="form-label form-label-sm">Header (cm)</label>
<input type="number" class="form-control form-control-sm" id="tpl_margin_header" step="0.1" min="0" max="25" value="<?= esc($marginHeader) ?>">
</div>
</div>
<div class="mb-2">
<label class="form-label form-label-sm">Header HTML</label>
<textarea id="tpl_header_html" class="form-control form-control-sm" rows="4" style="font-family:Consolas,monospace;"><?= esc($headerHtml) ?></textarea>
</div>
<div class="mb-2">
<label class="form-label form-label-sm">Footer HTML</label>
<textarea id="tpl_footer_html" class="form-control form-control-sm" rows="3" style="font-family:Consolas,monospace;"><?= esc($footerHtml) ?></textarea>
</div>
<div class="mb-2">
<label class="form-label form-label-sm">HTML Content <span class="text-muted small">(use <code>{{CONTENT}}</code> or section placeholders)</span></label>
<textarea id="tpl_html" class="form-control form-control-sm" rows="10" style="font-family:Consolas,monospace;"><?= esc($templateHtml) ?></textarea>
</div>
<div class="mb-2">
<label class="form-label form-label-sm">CSS</label>
<textarea id="tpl_css" class="form-control form-control-sm" rows="5" style="font-family:Consolas,monospace;"><?= esc($templateCss) ?></textarea>
</div>
<div class="d-flex gap-2 mb-2">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="tpl_is_default" <?= $isDefault ? 'checked' : '' ?>>
<label class="form-check-label" for="tpl_is_default">Set as default</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="tpl_is_audit" <?= $isAuditOnly ? 'checked' : '' ?>>
<label class="form-check-label" for="tpl_is_audit">Audit-only (NABH)</label>
</div>
</div>
<div class="d-flex gap-2">
<button type="button" class="btn btn-primary btn-sm" id="btn_save_tpl">
<?= $editId > 0 ? 'Update Template' : 'Create Template' ?>
</button>
<div class="ms-auto">
<label class="form-label form-label-sm mb-0">Preview IPD ID</label>
<input type="number" min="1" class="form-control form-control-sm d-inline-block" id="preview_ipd_id" style="width:100px;" placeholder="IPD ID">
<button type="button" class="btn btn-outline-primary btn-sm" id="btn_preview">Preview</button>
<button type="button" class="btn btn-outline-danger btn-sm" id="btn_pdf">PDF</button>
</div>
</div>
</div>
</div>
</div>
<!-- Right: Placeholder reference -->
<div class="col-md-6">
<div class="card">
<div class="card-header py-2"><strong>Placeholders</strong></div>
<div class="card-body" style="max-height:80vh;overflow-y:auto;">
<input type="text" class="form-control form-control-sm mb-2" id="ph_search" placeholder="Search placeholders…">
<?php foreach ($placeholders as $group => $items): ?>
<div class="mb-2 ph-group">
<div class="text-muted small fw-bold mb-1"><?= esc($group) ?></div>
<?php foreach ($items as $token => $desc): ?>
<span class="badge text-bg-light border me-1 mb-1 ph-chip" style="cursor:pointer;font-size:11px;" data-token="{{<?= esc($token) ?>}}" title="<?= esc($desc) ?>">{{<?= esc($token) ?>}}</span>
<?php endforeach; ?>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
</section>
<script>
(function () {
var editId = parseInt(document.getElementById('tpl_edit_id').value || '0', 10);
var msg = document.getElementById('tpl_edit_msg');
function getCsrf() {
var el = document.querySelector('input[name^="csrf_"]');
return el ? {name: el.name, value: el.value} : {name: 'csrf_test_name', value: ''};
}
function updateCsrf(data) {
if (!data || !data.csrfName) return;
var el = document.querySelector('input[name="' + data.csrfName + '"]');
if (el) el.value = data.csrfHash || '';
}
function showMsg(text, ok) {
if (!msg) return;
msg.innerHTML = '<div class="alert alert-' + (ok ? 'success' : 'danger') + ' py-2 mb-0">' + text + '</div>';
}
function val(id) { var el = document.getElementById(id); return el ? el.value : ''; }
function checked(id) { var el = document.getElementById(id); return el ? (el.checked ? 1 : 0) : 0; }
document.getElementById('btn_save_tpl').addEventListener('click', function () {
var tplName = val('tpl_name').trim();
var tplHtml = val('tpl_html').trim();
if (!tplName || !tplHtml) { showMsg('Template name and HTML content are required.', false); return; }
var csrf = getCsrf();
var fd = new FormData();
fd.append(csrf.name, csrf.value);
if (editId > 0) fd.append('id', String(editId));
fd.append('template_name', tplName);
fd.append('header_html', val('tpl_header_html'));
fd.append('footer_html', val('tpl_footer_html'));
fd.append('template_css', val('tpl_css'));
fd.append('template_html', tplHtml);
fd.append('page_size', val('tpl_page_size'));
fd.append('page_margin_top_cm', val('tpl_margin_top'));
fd.append('page_margin_bottom_cm', val('tpl_margin_bottom'));
fd.append('page_margin_left_cm', val('tpl_margin_left'));
fd.append('page_margin_right_cm', val('tpl_margin_right'));
fd.append('margin_header_cm', val('tpl_margin_header'));
fd.append('margin_footer_cm', '0.5');
fd.append('status', val('tpl_status'));
fd.append('is_default', String(checked('tpl_is_default')));
fd.append('is_audit_only', String(checked('tpl_is_audit')));
fetch('<?= base_url('setting/template/discharge_templates') ?>', {
method: 'POST',
headers: {'X-Requested-With': 'XMLHttpRequest'},
body: fd
}).then(function (r) { return r.json(); }).then(function (data) {
updateCsrf(data);
if (data.update == 1) {
showMsg(editId > 0 ? 'Template updated.' : 'Template created.', true);
if (!editId) {
load_form('<?= base_url('Ipd_discharge/print_template_builder') ?>?mode=edit&edit=' + encodeURIComponent(data.id || 0), 'IPD Discharge Template Edit');
}
} else {
showMsg(data.error_text || 'Save failed.', false);
}
}).catch(function () { showMsg('Request failed.', false); });
});
document.getElementById('btn_preview').addEventListener('click', function () {
var ipdId = parseInt(val('preview_ipd_id'), 10);
if (!ipdId) { alert('Enter IPD ID for preview.'); return; }
window.open('<?= base_url('Ipd_discharge/preview_discharge_report') ?>/' + ipdId + (editId > 0 ? '?tpl=' + editId : ''), '_blank');
});
document.getElementById('btn_pdf').addEventListener('click', function () {
var ipdId = parseInt(val('preview_ipd_id'), 10);
if (!ipdId) { alert('Enter IPD ID for PDF.'); return; }
window.open('<?= base_url('Ipd_discharge/show_discharge') ?>/' + ipdId + '/1' + (editId > 0 ? '?tpl=' + editId : ''), '_blank');
});
/* Placeholder click-to-copy */
document.querySelectorAll('.ph-chip').forEach(function (chip) {
chip.addEventListener('click', function () {
var token = chip.getAttribute('data-token');
var active = document.activeElement;
var targets = ['tpl_header_html', 'tpl_footer_html', 'tpl_html', 'tpl_css'];
var target = null;
targets.forEach(function (id) { if (active && active.id === id) target = active; });
if (!target) target = document.getElementById('tpl_html');
if (!target) return;
var start = target.selectionStart;
var end = target.selectionEnd;
target.setRangeText(token, start, end, 'end');
target.focus();
});
});
/* Placeholder search */
document.getElementById('ph_search').addEventListener('input', function () {
var q = this.value.toLowerCase();
document.querySelectorAll('.ph-chip').forEach(function (chip) {
chip.style.display = !q || chip.dataset.token.toLowerCase().includes(q) ? '' : 'none';
});
document.querySelectorAll('.ph-group').forEach(function (group) {
var visible = [...group.querySelectorAll('.ph-chip')].some(function (c) { return c.style.display !== 'none'; });
group.style.display = visible ? '' : 'none';
});
});
})();
</script>

View File

@ -0,0 +1,152 @@
<?php $rows = $rows ?? []; ?>
<section class="content-header">
<div class="d-flex justify-content-between align-items-center">
<h1 class="mb-0">IPD Discharge Template Builder</h1>
<button type="button" class="btn btn-success btn-sm" id="btn_new_tpl">+ Add New Template</button>
</div>
</section>
<section class="content mt-3">
<div class="card">
<div class="card-body">
<?= csrf_field() ?>
<div id="tpl_list_msg" class="mb-2"></div>
<div class="table-responsive">
<table class="table table-bordered table-striped">
<thead>
<tr>
<th style="width:60px;">#</th>
<th>Template Name</th>
<th style="width:80px;">Default</th>
<th style="width:80px;">Audit</th>
<th style="width:200px;">Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($rows)): ?>
<tr><td colspan="5" class="text-center text-muted">No templates. Click Add New.</td></tr>
<?php else: ?>
<?php foreach ($rows as $i => $row): ?>
<tr>
<td><?= (int) $i + 1 ?></td>
<td><?= esc((string) ($row['template_name'] ?? '')) ?></td>
<td><?= (int) ($row['is_default'] ?? 0) ? '<span class="badge bg-success">Yes</span>' : '' ?></td>
<td><?= (int) ($row['is_audit_only'] ?? 0) ? '<span class="badge bg-warning text-dark">Audit</span>' : '' ?></td>
<td>
<a class="btn btn-primary btn-sm"
href="javascript:load_form('<?= base_url('Ipd_discharge/print_template_builder') ?>?mode=edit&edit=<?= (int) ($row['id'] ?? 0) ?>','IPD Discharge Template Edit');">Edit</a>
<button type="button" class="btn btn-outline-secondary btn-sm btn_rename" data-id="<?= (int) ($row['id'] ?? 0) ?>" data-name="<?= esc((string) ($row['template_name'] ?? '')) ?>">Rename</button>
<button type="button" class="btn btn-danger btn-sm btn_delete" data-id="<?= (int) ($row['id'] ?? 0) ?>">Delete</button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</section>
<script>
(function () {
var listMsg = document.getElementById('tpl_list_msg');
function getCsrf() {
var el = document.querySelector('input[name^="csrf_"]');
return el ? {name: el.name, value: el.value} : {name: 'csrf_test_name', value: ''};
}
function showMsg(msg, ok) {
if (!listMsg) return;
listMsg.className = 'mb-2';
listMsg.innerHTML = '<div class="alert alert-' + (ok ? 'success' : 'danger') + ' py-2 mb-0">' + msg + '</div>';
}
function updateCsrf(data) {
if (!data || !data.csrfName) return;
var el = document.querySelector('input[name="' + data.csrfName + '"]');
if (el) el.value = data.csrfHash || '';
}
document.getElementById('btn_new_tpl').addEventListener('click', function () {
var name = window.prompt('New template name (letters, numbers, spaces):', '');
if (!name) return;
name = name.trim();
if (!name) { showMsg('Name is required.', false); return; }
var csrf = getCsrf();
var fd = new FormData();
fd.append(csrf.name, csrf.value);
fd.append('template_name', name);
fd.append('template_html', '{{CONTENT}}');
fd.append('page_size', 'A4');
fd.append('status', '1');
fetch('<?= base_url('setting/template/discharge_templates') ?>', {
method: 'POST',
headers: {'X-Requested-With': 'XMLHttpRequest'},
body: fd
}).then(function (r) { return r.json(); }).then(function (data) {
updateCsrf(data);
if (data.update == 1) {
load_form('<?= base_url('Ipd_discharge/print_template_builder') ?>?mode=edit&edit=' + encodeURIComponent(data.id || 0), 'IPD Discharge Template Edit');
} else {
showMsg(data.error_text || 'Unable to create template.', false);
}
}).catch(function () { showMsg('Request failed.', false); });
});
document.querySelectorAll('.btn_rename').forEach(function (btn) {
btn.addEventListener('click', function () {
var id = btn.getAttribute('data-id');
var oldName = btn.getAttribute('data-name');
var newName = window.prompt('Rename template:', oldName);
if (!newName || newName.trim() === oldName) return;
newName = newName.trim();
var csrf = getCsrf();
var fd = new FormData();
fd.append(csrf.name, csrf.value);
fd.append('id', id);
fd.append('template_name', newName);
fetch('<?= base_url('Ipd_discharge/discharge_template_rename_ajax') ?>', {
method: 'POST',
headers: {'X-Requested-With': 'XMLHttpRequest'},
body: fd
}).then(function (r) { return r.json(); }).then(function (data) {
updateCsrf(data);
if (data.update == 1) {
load_form('<?= base_url('Ipd_discharge/print_template_builder') ?>?mode=list', 'IPD Discharge Templates');
} else {
showMsg(data.error_text || 'Unable to rename.', false);
}
}).catch(function () { showMsg('Request failed.', false); });
});
});
document.querySelectorAll('.btn_delete').forEach(function (btn) {
btn.addEventListener('click', function () {
if (!confirm('Delete this template?')) return;
var id = btn.getAttribute('data-id');
var csrf = getCsrf();
var fd = new FormData();
fd.append(csrf.name, csrf.value);
fetch('<?= base_url('setting/template/discharge_templates/delete') ?>/' + id, {
method: 'POST',
headers: {'X-Requested-With': 'XMLHttpRequest'},
body: fd
}).then(function (r) { return r.json(); }).then(function (data) {
updateCsrf(data);
if (data.update == 1) {
load_form('<?= base_url('Ipd_discharge/print_template_builder') ?>?mode=list', 'IPD Discharge Templates');
} else {
showMsg(data.error_text || 'Unable to delete.', false);
}
}).catch(function () { showMsg('Request failed.', false); });
});
});
})();
</script>