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:
parent
16348d12db
commit
550e72eca8
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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([
|
||||
|
|
|
|||
293
app/Views/ipd_discharge/search_patient.php
Normal file
293
app/Views/ipd_discharge/search_patient.php
Normal 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 = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
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>
|
||||
|
|
@ -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) { ?>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user