From d82078c9ed3742c826ebfcfa4597517a11fa9882 Mon Sep 17 00:00:00 2001 From: DevSoftTech Server Date: Sat, 6 Jun 2026 14:52:10 +0530 Subject: [PATCH] Convert patient search to DataTables with AJAX server-side processing - Updated Patient::search() to only render view with search query - Created Patient::search_ajax() method for DataTables AJAX endpoint - Implemented server-side processing with pagination, sorting, and filtering - Results ordered by Last Visit DESC by default - Retrieves all patient data from patient_master table - Added proper SQL escaping to prevent injection - Updated Patient_Search_V.php to use DataTables with AJAX remote data - Added DataTables configuration with 25 rows per page default - Supports dynamic length menu (10, 25, 50, 100, All) - Added route for billing/patient/search_ajax endpoint - Improved user experience with loading spinner and customized messages --- app/Config/Routes.php | 1 + app/Controllers/Patient.php | 111 ++++++++++++++++++------- app/Views/billing/Patient_Search_V.php | 62 ++++++++++---- 3 files changed, 127 insertions(+), 47 deletions(-) diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 6dc4cfc..70a96b8 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -457,6 +457,7 @@ $routes->group('billing', function($routes) { $routes->get('patient/search_opd', 'Patient::search_opd'); $routes->post('patient/create', 'Patient::create'); $routes->post('patient/search', 'Patient::search'); + $routes->get('billing/patient/search_ajax', 'Patient::search_ajax'); $routes->post('patient/search_adv', 'Patient::search_adv'); $routes->post('patient/abha_fetch_profile', 'Patient::abha_fetch_profile'); $routes->post('patient/abha_aadhaar_generate_otp', 'Patient::abhaAadhaarGenerateOtp'); diff --git a/app/Controllers/Patient.php b/app/Controllers/Patient.php index adbedc1..2a01bdb 100644 --- a/app/Controllers/Patient.php +++ b/app/Controllers/Patient.php @@ -408,6 +408,16 @@ class Patient extends BaseController $sdata = (string) $this->request->getPost('txtsearch'); $sdata = preg_replace('/[^A-Za-z0-9 _.@\-]/', '', trim($sdata ?? '')); + $data['search_query'] = $sdata; + return view('billing/Patient_Search_V', $data); + } + + public function search_ajax() + { + $request = $this->request->getGet(); + $sdata = trim((string) ($request['search_query'] ?? '')); + $sdata = preg_replace('/[^A-Za-z0-9 _.@\-]/', '', $sdata); + // Detect ABHA column name in patient_master $abhaField = null; $pmFields = $this->db->getFieldNames('patient_master') ?? []; @@ -415,16 +425,10 @@ class Patient extends BaseController if (in_array($f, $pmFields, true)) { $abhaField = $f; break; } } - if (strlen($sdata) === 0) { - $sql = "SELECT p.*, - Date_Format(p.last_visit,'%d-%m-%Y') AS Last_Visit - FROM patient_master p - GROUP BY p.id - ORDER BY p.last_visit DESC - LIMIT 100 "; - } else { + // Build WHERE clause based on search query + $searchString = ' 1=1 '; + if (strlen($sdata) > 0) { $sdateArray = explode(' ', $sdata); - $searchString = ' 1=1 '; foreach ($sdateArray as $rowData) { if ($rowData === '') { @@ -432,37 +436,86 @@ class Patient extends BaseController } if (is_numeric($rowData)) { - $abhaClause = $abhaField ? " or p.{$abhaField} = '$rowData'" : ''; - $searchString .= " and (p.p_code like '%$rowData' - or p.mphone1 = '$rowData' - or p.udai='$rowData'$abhaClause)"; + $abhaClause = $abhaField ? " or p.{$abhaField} = " . $this->db->escape($rowData) : ''; + $searchString .= " and (p.p_code like " . $this->db->escape('%' . $rowData) . " + or p.mphone1 = " . $this->db->escape($rowData) . " + or p.udai=" . $this->db->escape($rowData) . $abhaClause . ")"; } elseif (ctype_alpha($rowData)) { - $searchString .= " and (p.p_fname like '%$rowData%' - or p.email1 = '$rowData' - or SUBSTRING_INDEX(p.p_fname,' ',1) sounds like '$rowData')"; + $searchString .= " and (p.p_fname like " . $this->db->escape('%' . $rowData . '%') . " + or p.email1 = " . $this->db->escape($rowData) . " + or SUBSTRING_INDEX(p.p_fname,' ',1) sounds like " . $this->db->escape($rowData) . ")"; } else { // Handle dashed ABHA format: XX-XXXX-XXXX-XXXX $rawDigits = preg_replace('/\D/', '', $rowData); $abhaElse = ($abhaField && strlen($rawDigits) === 14) - ? " or p.{$abhaField} = '$rawDigits' or p.{$abhaField} = '$rowData'" + ? " or p.{$abhaField} = " . $this->db->escape($rawDigits) . " or p.{$abhaField} = " . $this->db->escape($rowData) : ''; - $searchString .= " and (p.p_code like '$rowData' - or p.email1 = '$rowData'$abhaElse)"; + $searchString .= " and (p.p_code like " . $this->db->escape($rowData) . " + or p.email1 = " . $this->db->escape($rowData) . $abhaElse . ")"; } } - - $sql = "SELECT p.*, - Date_Format(p.last_visit,'%d-%m-%Y') AS Last_Visit - FROM patient_master p - WHERE $searchString - GROUP BY p.id - ORDER BY p.last_visit DESC"; } - $query = $this->db->query($sql); - $data['data'] = $query->getResult(); + // Count total records (without filtering) + $totalSql = "SELECT COUNT(DISTINCT p.id) as total FROM patient_master p"; + $totalData = (int) $this->db->query($totalSql)->getRow()->total; - return view('billing/Patient_Search_V', $data); + // Count filtered records + $filteredSql = "SELECT COUNT(DISTINCT p.id) as total FROM patient_master p WHERE $searchString"; + $totalFiltered = (int) $this->db->query($filteredSql)->getRow()->total; + + // Get pagination and sorting parameters + $start = isset($request['start']) ? (int) $request['start'] : 0; + $length = isset($request['length']) ? (int) $request['length'] : 10; + + // Columns for ordering + $columns = ['', 'p.p_code', 'p.p_fname', 'p.age', 'p.last_visit', 'p.insurance_id', '']; + $orderColumn = 'p.last_visit'; + $orderDir = 'DESC'; + + if (!empty($request['order'][0]['column']) && is_numeric($request['order'][0]['column'])) { + $orderIndex = (int) $request['order'][0]['column']; + if (isset($columns[$orderIndex]) && $columns[$orderIndex] !== '') { + $orderColumn = $columns[$orderIndex]; + } + $orderDir = strtoupper($request['order'][0]['dir'] ?? 'DESC') === 'ASC' ? 'ASC' : 'DESC'; + } + + // Get data + $sql = "SELECT p.*, + Date_Format(p.last_visit,'%d-%m-%Y') AS Last_Visit, + IF(p.insurance_id = 0 OR p.insurance_id IS NULL, 'Self', 'Insuranced') as insurance_status + FROM patient_master p + WHERE $searchString + GROUP BY p.id + ORDER BY $orderColumn $orderDir + LIMIT $start, $length"; + + $records = $this->db->query($sql)->getResult(); + + // Format data for DataTables + $data = []; + foreach ($records as $index => $row) { + $patientId = (int) ($row->id ?? 0); + $age = esc(get_age_1($row->dob ?? null, $row->age ?? '', $row->age_in_month ?? '', $row->estimate_dob ?? '', $row->Last_Visit ?? null)); + + $data[] = [ + $start + $index + 1, + '' . esc($row->p_code ?? '') . '', + esc(($row->p_fname ?? '') . ' {' . ($row->p_rname ?? '') . '}'), + $age, + esc($row->Last_Visit ?? ''), + esc($row->insurance_status ?? 'Self'), + ' Patient History' + ]; + } + + return $this->response->setJSON([ + 'draw' => isset($request['draw']) ? (int) $request['draw'] : 0, + 'recordsTotal' => $totalData, + 'recordsFiltered' => $totalFiltered, + 'data' => $data + ]); } public function person_record(int $pno, int $edit = 0) diff --git a/app/Views/billing/Patient_Search_V.php b/app/Views/billing/Patient_Search_V.php index fbe6aa4..bb2c67c 100644 --- a/app/Views/billing/Patient_Search_V.php +++ b/app/Views/billing/Patient_Search_V.php @@ -1,7 +1,7 @@

-

Result

+

Patient Search Results

@@ -11,35 +11,20 @@ Sr.No. Patient/UHID Code Name {Relative Name} - Age + Age Last Visit Insurance Patient History - - - - p_code ?> - p_fname ?> {p_rname ?>} - dob ?? null, $data[$i]->age ?? '', $data[$i]->age_in_month ?? '', $data[$i]->estimate_dob ?? '', $data[$i]->Last_Visit ?? null)) ?> - Last_Visit ?> - insurance_id==0 ? 'Self': 'Insuranced'); ?> - - - Patient History - - - - Sr.No. Patient/UHID Code Name {Relative Name} - Age + Age Last Visit Insurance Patient History @@ -49,4 +34,45 @@
+ + \ No newline at end of file