Add IPD Discharge search functionality with AJAX

- Added 'Discharge' button in sidebar under In-Patient & Nursing Care section
- Created search_patient() and search_patient_ajax() controller methods
- Implemented AJAX-based patient search by IPD Code, UHID, Name, or Mobile
- Added ipd_discharge/search_patient.php view with dynamic results table
- Search results show patient details, bed info, doctor assignments, and discharge status
- Added 'Create Discharge' and 'Print Discharge' action buttons for each patient
- Integrated DataTables for sorting and pagination
- Fixed database column names (patient_master.gender, age, age_in_month)
- Added routes for search page and AJAX endpoint
This commit is contained in:
DevSoftTech Server 2026-06-06 14:43:10 +05:30
parent 16348d12db
commit 550e72eca8
4 changed files with 408 additions and 0 deletions

View File

@ -366,6 +366,8 @@ $routes->get('ipdNew/show_ipd_form/(:num)', 'Billing\\Ipd::showIpdForm/$1/1');
$routes->get('ipdNew/show_ipd_form/(:num)/(:num)', 'Billing\\Ipd::showIpdForm/$1/$2');
// Legacy compatibility: old discharge summary links should open current discharge tab.
$routes->get('Ipd_discharge/search_patient', 'Ipd_discharge::search_patient');
$routes->get('Ipd_discharge/search_patient_ajax', 'Ipd_discharge::search_patient_ajax');
$routes->match(['get', 'post'], 'Ipd_discharge/ipd_select/(:num)', 'Ipd_discharge::ipd_select/$1');
$routes->match(['get', 'post'], 'Ipd_discharge/ipd_select/(:num)/(:num)', 'Ipd_discharge::ipd_select/$1/$2');
$routes->get('Ipd_discharge/section_past_data', 'Ipd_discharge::section_past_data');

View File

@ -4371,6 +4371,113 @@ class Ipd_discharge extends BaseController
->update($update);
}
public function search_patient()
{
$permission = $this->requireAnyPermission([
'billing.access',
'billing.ipd.invoice',
'billing.ipd.current-admission',
]);
if ($permission) {
return $permission;
}
return view('ipd_discharge/search_patient');
}
public function search_patient_ajax()
{
$permission = $this->requireAnyPermission([
'billing.access',
'billing.ipd.invoice',
'billing.ipd.current-admission',
]);
if ($permission) {
return $this->response->setJSON(['success' => false, 'message' => 'Access denied']);
}
$searchQuery = trim((string) ($this->request->getGet('q') ?? ''));
if ($searchQuery === '') {
return $this->response->setJSON([
'success' => false,
'message' => 'Please enter a search term'
]);
}
try {
// Escape search query for LIKE
$escapedSearch = $this->db->escapeLikeString($searchQuery);
$likePattern = '%' . $escapedSearch . '%';
// First, get basic IPD and patient data
$sql = "SELECT
im.id, im.ipd_code, im.register_date, im.discharge_date,
im.discarge_patient_status as discharge_status,
p.id as p_id, p.p_code as uhid, p.p_fname, p.p_rname,
IF(p.gender = 1, 'Male', 'Female') as xgender,
p.age as p_age,
p.age_in_month,
DATEDIFF(COALESCE(im.discharge_date, CURDATE()), im.register_date) as no_days
FROM ipd_master im
LEFT JOIN patient_master p ON p.id = im.p_id
WHERE (
im.ipd_code LIKE ? OR
p.p_code LIKE ? OR
p.p_fname LIKE ? OR
p.p_rname LIKE ? OR
p.mphone1 LIKE ?
)
ORDER BY im.id DESC
LIMIT 50";
$records = $this->db->query($sql, [
$likePattern, $likePattern, $likePattern, $likePattern, $likePattern
])->getResult();
// Enrich each record with bed and doctor info
foreach ($records as $record) {
$ipdId = (int) $record->id;
// Get bed info
$bedSql = "SELECT
CONCAT('Bed No :', COALESCE(b.bed_number, ''), ' [', COALESCE(w.ward_name, ''), ']') as Bed_Desc
FROM bed_assignment_history bah
LEFT JOIN bed_master b ON b.id = bah.bed_id
LEFT JOIN ward_master w ON w.id = bah.ward_id
WHERE bah.ipd_id = ?
ORDER BY bah.id DESC
LIMIT 1";
$bedResult = $this->db->query($bedSql, [$ipdId])->getRow();
$record->Bed_Desc = $bedResult->Bed_Desc ?? '';
// Get doctor info
$docSql = "SELECT
GROUP_CONCAT(DISTINCT CONCAT_WS(' ', 'Dr.', d.p_fname, d.p_mname, d.p_lname) SEPARATOR ', ') as doc_name
FROM ipd_master_doc_list i
JOIN doctor_master d ON i.doc_id = d.id
WHERE i.ipd_id = ?
GROUP BY i.ipd_id";
$docResult = $this->db->query($docSql, [$ipdId])->getRow();
$record->doc_name = $docResult->doc_name ?? '';
}
return $this->response->setJSON([
'success' => true,
'records' => $records,
'count' => count($records)
]);
} catch (\Throwable $e) {
log_message('error', 'IPD Discharge Search Error: ' . $e->getMessage() . ' | File: ' . $e->getFile() . ' | Line: ' . $e->getLine());
return $this->response->setJSON([
'success' => false,
'message' => 'Database error: ' . $e->getMessage()
]);
}
}
public function ipd_select(int $ipdId, int $reCreate = 0)
{
$permission = $this->requireAnyPermission([

View File

@ -0,0 +1,293 @@
<section class="content">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Search IPD Patient for Discharge</h5>
</div>
<div class="card-body">
<form id="ipdDischargeSearchForm" class="mb-4">
<div class="row g-3 align-items-end">
<div class="col-md-8">
<label for="search_input" class="form-label">Search by IPD Code, UHID, Patient Name, or Mobile</label>
<input type="text"
class="form-control"
id="search_input"
name="q"
value=""
placeholder="Enter IPD Code, UHID, Patient Name, or Mobile Number"
autofocus>
</div>
<div class="col-md-4">
<button type="submit" class="btn btn-primary">
<i class="bi bi-search"></i> Search
</button>
<button type="button" class="btn btn-outline-secondary" id="btnClearSearch">
<i class="bi bi-x-circle"></i> Clear
</button>
</div>
</div>
</form>
<div id="searchResultsContainer">
<div class="alert alert-light border">
<i class="bi bi-search"></i> Enter a search term above to find IPD patients.
</div>
</div>
</div>
</div>
</section>
<script>
(function () {
var searchInput = $('#search_input');
var searchForm = $('#ipdDischargeSearchForm');
var resultsContainer = $('#searchResultsContainer');
var dataTable = null;
function performSearch() {
var searchQuery = searchInput.val().trim();
if (searchQuery === '') {
resultsContainer.html(
'<div class="alert alert-light border">' +
'<i class="bi bi-search"></i> Enter a search term above to find IPD patients.' +
'</div>'
);
return;
}
// Show loading
resultsContainer.html(
'<div class="text-center py-4">' +
'<div class="spinner-border text-primary" role="status">' +
'<span class="visually-hidden">Loading...</span>' +
'</div>' +
'<p class="mt-2">Searching...</p>' +
'</div>'
);
$.ajax({
url: '<?= base_url('Ipd_discharge/search_patient_ajax') ?>',
method: 'GET',
data: { q: searchQuery },
dataType: 'json',
success: function(response) {
if (response.success) {
displayResults(response.records, searchQuery);
} else {
resultsContainer.html(
'<div class="alert alert-danger">' +
'<i class="bi bi-exclamation-triangle"></i> ' + (response.message || 'Error occurred during search') +
'</div>'
);
}
},
error: function(xhr) {
resultsContainer.html(
'<div class="alert alert-danger">' +
'<i class="bi bi-exclamation-triangle"></i> Failed to perform search. Please try again.' +
'</div>'
);
}
});
}
function displayResults(records, searchQuery) {
if (!records || records.length === 0) {
resultsContainer.html(
'<div class="alert alert-info">' +
'<i class="bi bi-info-circle"></i> No patients found matching "' + escapeHtml(searchQuery) + '".' +
'</div>'
);
return;
}
var html = '<div class="alert alert-success">' +
'Found ' + records.length + ' patient(s) matching "' + escapeHtml(searchQuery) + '".' +
'</div>' +
'<div class="table-responsive">' +
'<table class="table table-striped table-bordered table-hover" id="ipdDischargeSearchTable">' +
'<thead>' +
'<tr>' +
'<th>IPD Code</th>' +
'<th>UHID</th>' +
'<th>Patient Name</th>' +
'<th>Age/Gender</th>' +
'<th>Bed</th>' +
'<th>Doctor</th>' +
'<th>Admit Date</th>' +
'<th>Days</th>' +
'<th>Status</th>' +
'<th>Actions</th>' +
'</tr>' +
'</thead>' +
'<tbody>';
records.forEach(function(row) {
var patientName = (row.p_fname || '') + ' ' + (row.p_rname || '');
var age = parseInt(row.p_age || 0);
var ageInMonth = parseInt(row.age_in_month || 0);
// Determine age display
var ageDisplay = '';
if (age > 0) {
ageDisplay = age + 'Y';
} else if (ageInMonth > 0) {
if (ageInMonth >= 12) {
ageDisplay = Math.floor(ageInMonth / 12) + 'Y';
} else if (ageInMonth > 1) {
ageDisplay = ageInMonth + 'M';
} else {
ageDisplay = '1M';
}
} else {
ageDisplay = '0Y';
}
var gender = (row.xgender || 'Unknown').charAt(0).toUpperCase();
var ageGender = ageDisplay + '/' + gender;
var dischargeStatus = parseInt(row.discharge_status || 0);
var statusBadge = '';
switch (dischargeStatus) {
case 1:
statusBadge = '<span class="badge bg-success">Discharged</span>';
break;
case 2:
statusBadge = '<span class="badge bg-info">LAMA</span>';
break;
case 3:
statusBadge = '<span class="badge bg-danger">Death</span>';
break;
case 4:
statusBadge = '<span class="badge bg-warning">Absconding</span>';
break;
default:
statusBadge = '<span class="badge bg-primary">Admitted</span>';
}
var ipdId = parseInt(row.id || 0);
var admitDate = row.register_date ? formatDate(row.register_date) : '';
html += '<tr>' +
'<td>' + escapeHtml(row.ipd_code || '') + '</td>' +
'<td>' + escapeHtml(row.uhid || '') + '</td>' +
'<td>' + escapeHtml(patientName.trim()) + '</td>' +
'<td>' + escapeHtml(ageGender) + '</td>' +
'<td>' + escapeHtml(row.Bed_Desc || '') + '</td>' +
'<td>' + escapeHtml(row.doc_name || '') + '</td>' +
'<td>' + admitDate + '</td>' +
'<td>' + escapeHtml(row.no_days || '0') + '</td>' +
'<td>' + statusBadge + '</td>' +
'<td class="text-nowrap">' +
'<button type="button" class="btn btn-success btn-sm me-1" ' +
'onclick="load_form(\'<?= base_url('Ipd_discharge/ipd_select/') ?>' + ipdId + '\',\'Create Discharge - ' + escapeHtml(row.ipd_code || '') + '\');" ' +
'title="Create/Edit Discharge Summary">' +
'<i class="bi bi-file-earmark-medical"></i> Create Discharge' +
'</button>' +
'<button type="button" class="btn btn-primary btn-sm" ' +
'onclick="openDischargePrintWindow(' + ipdId + ')" ' +
'title="Print Discharge Summary">' +
'<i class="bi bi-printer"></i> Print Discharge' +
'</button>' +
'</td>' +
'</tr>';
});
html += '</tbody></table></div>';
resultsContainer.html(html);
// Destroy existing DataTable if any
if (dataTable) {
dataTable.destroy();
}
// Initialize DataTable
if (window.jQuery && $.fn.DataTable) {
dataTable = $('#ipdDischargeSearchTable').DataTable({
pageLength: 25,
order: [[0, 'desc']],
columnDefs: [
{ orderable: false, targets: -1 }
]
});
}
}
function escapeHtml(text) {
var map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return String(text).replace(/[&<>"']/g, function(m) { return map[m]; });
}
function formatDate(dateStr) {
try {
var date = new Date(dateStr);
var day = ('0' + date.getDate()).slice(-2);
var month = ('0' + (date.getMonth() + 1)).slice(-2);
var year = date.getFullYear();
return day + '-' + month + '-' + year;
} catch (e) {
return dateStr;
}
}
// Form submit handler
searchForm.on('submit', function(e) {
e.preventDefault();
performSearch();
});
// Clear button handler
$('#btnClearSearch').on('click', function() {
searchInput.val('');
resultsContainer.html(
'<div class="alert alert-light border">' +
'<i class="bi bi-search"></i> Enter a search term above to find IPD patients.' +
'</div>'
);
searchInput.focus();
});
// Global function for print window
window.openDischargePrintWindow = function(ipdId) {
if (!ipdId || ipdId <= 0) {
alert('Invalid IPD ID');
return;
}
var url = '<?= base_url('Ipd_discharge/show_discharge') ?>/' + ipdId + '/1';
var printWindow = window.open(url, 'DischargePrint', 'width=900,height=700,scrollbars=yes,resizable=yes');
if (printWindow) {
printWindow.focus();
} else {
alert('Please allow pop-ups to print the discharge summary.');
}
};
})();
</script>
<style>
#ipdDischargeSearchTable {
font-size: 0.9rem;
}
#ipdDischargeSearchTable th {
background-color: #f8f9fa;
font-weight: 600;
}
#ipdDischargeSearchTable td {
vertical-align: middle;
}
.btn-sm {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
}
</style>

View File

@ -315,6 +315,12 @@ if (! $canChargesSettings && $user && method_exists($user, 'inGroup')) {
<span>IPD Patient List</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link collapsed" href="javascript:load_form('<?= base_url('Ipd_discharge/search_patient') ?>','Discharge Management')">
<i class="bi bi-file-medical"></i>
<span>Discharge</span>
</a>
</li>
<?php } ?>
<?php if ($canDoctorWork) { ?>