From 771fa647780ae568f467fa5a280cb7ff0d00cec8 Mon Sep 17 00:00:00 2001 From: DevSoftTech Server Date: Wed, 1 Apr 2026 18:26:33 +0530 Subject: [PATCH] Investigation Master: CRUD, favourites, specialization filter --- app/Config/Routes.php | 7 + app/Controllers/Opd_prescription.php | 281 +++++++++++- ...01-000031_AddInvestigationMasterFields.php | 46 ++ .../billing/opd_invest_master_workspace.php | 401 ++++++++++++++++++ app/Views/billing/opd_prescription_basic.php | 126 +++++- app/Views/partials/sidebar.php | 7 + 6 files changed, 854 insertions(+), 14 deletions(-) create mode 100644 app/Database/Migrations/2026-04-01-000031_AddInvestigationMasterFields.php create mode 100644 app/Views/billing/opd_invest_master_workspace.php diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 8810f0a..81f8d15 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -594,6 +594,13 @@ $routes->get('Opd_prescription/investigation_list/(:num)', 'Opd_prescription::in $routes->get('Opd_prescription/investigation_list/(:num)/(:num)', 'Opd_prescription::investigation_list/$1/$2'); $routes->post('Opd_prescription/investigation_add', 'Opd_prescription::investigation_add'); $routes->post('Opd_prescription/investigation_remove/(:num)', 'Opd_prescription::investigation_remove/$1'); +$routes->get('Opd_prescription/opd_invest_master', 'Opd_prescription::opd_invest_master', ['filter' => 'permission:doctor_work.medicine.manage,doctor_work.access']); +$routes->get('Opd_prescription/opd_invest_master_specs', 'Opd_prescription::opd_invest_master_specs'); +$routes->get('Opd_prescription/opd_invest_master_data', 'Opd_prescription::opd_invest_master_data'); +$routes->get('Opd_prescription/opd_invest_master_get/(:num)', 'Opd_prescription::opd_invest_master_get/$1'); +$routes->post('Opd_prescription/opd_invest_master_save', 'Opd_prescription::opd_invest_master_save'); +$routes->post('Opd_prescription/opd_invest_master_remove/(:num)', 'Opd_prescription::opd_invest_master_remove/$1'); +$routes->post('Opd_prescription/opd_invest_master_toggle_fav/(:num)', 'Opd_prescription::opd_invest_master_toggle_fav/$1'); $routes->get('Opd_prescription/medicine_search', 'Opd_prescription::medicine_search'); $routes->get('Opd_prescription/medicine_substitutes', 'Opd_prescription::medicine_substitutes'); diff --git a/app/Controllers/Opd_prescription.php b/app/Controllers/Opd_prescription.php index 3e48b36..7343060 100644 --- a/app/Controllers/Opd_prescription.php +++ b/app/Controllers/Opd_prescription.php @@ -3043,6 +3043,235 @@ class Opd_prescription extends BaseController return $this->response->setJSON(['update' => 1, 'error_text' => 'Advice removed']); } + // ─── Investigation Master CRUD ───────────────────────────────────────── + + public function opd_invest_master() + { + $doctorModel = new \App\Models\DoctorModel($this->db); + return view('billing/opd_invest_master_workspace', [ + 'initial_inv_id' => 0, + 'med_specs' => $doctorModel->getMedSpecs(), + ]); + } + + public function opd_invest_master_specs() + { + $specs = []; + if ($this->db->tableExists('med_spec')) { + $specs = $this->db->table('med_spec') + ->select('id, SpecName') + ->orderBy('SpecName', 'ASC') + ->get()->getResultArray(); + } + return $this->response->setJSON(['specs' => $specs]); + } + + public function opd_invest_master_data() + { + if (! $this->db->tableExists('investigation')) { + return $this->response->setJSON(['draw' => 1, 'recordsTotal' => 0, 'recordsFiltered' => 0, 'data' => []]); + } + + $fields = $this->db->getFieldNames('investigation'); + $codeField = $this->resolveFirstField($fields, ['Code', 'code']); + $nameField = $this->resolveFirstField($fields, ['Name', 'name']); + if ($codeField === null || $nameField === null) { + return $this->response->setJSON(['draw' => 1, 'recordsTotal' => 0, 'recordsFiltered' => 0, 'data' => []]); + } + + $filter = trim((string) $this->request->getGet('filter')); + $specId = (int) $this->request->getGet('spec_id'); + $favOnly = (int) $this->request->getGet('fav_only'); + $draw = (int) $this->request->getGet('draw'); + $start = max(0, (int) $this->request->getGet('start')); + $length = (int) $this->request->getGet('length'); + if ($length <= 0) { + $length = 25; + } + + $selectCols = 'id, ' . $codeField . ' as code, ' . $nameField . ' as name'; + foreach (['short_name', 'is_favourite', 'spec_ids', 'category_name'] as $f) { + if (in_array($f, $fields, true)) { + $selectCols .= ', ' . $f; + } + } + + $applyFilters = function ($b) use ($filter, $favOnly, $specId, $fields, $nameField, $codeField) { + if ($filter !== '') { + $b->groupStart() + ->like($nameField, $filter) + ->orLike($codeField, $filter); + if (in_array('short_name', $fields, true)) { + $b->orLike('short_name', $filter); + } + $b->groupEnd(); + } + if ($favOnly && in_array('is_favourite', $fields, true)) { + $b->where('is_favourite', 1); + } + if ($specId > 0 && in_array('spec_ids', $fields, true)) { + $b->where("FIND_IN_SET({$specId}, spec_ids) > 0", null, false); + } + }; + + $total = $this->db->table('investigation')->countAllResults(); + + $countBuilder = $this->db->table('investigation'); + $applyFilters($countBuilder); + $filtered = $countBuilder->countAllResults(); + + $builder = $this->db->table('investigation')->select($selectCols); + $applyFilters($builder); + if (in_array('is_favourite', $fields, true)) { + $builder->orderBy('is_favourite', 'DESC'); + } + $builder->orderBy($nameField, 'ASC'); + $builder->limit($length, $start); + $rows = $builder->get()->getResultArray(); + + return $this->response->setJSON([ + 'draw' => $draw, + 'recordsTotal' => $total, + 'recordsFiltered' => $filtered, + 'data' => $rows, + ]); + } + + public function opd_invest_master_get(int $invId) + { + if ($invId <= 0 || ! $this->db->tableExists('investigation')) { + return $this->response->setJSON(['update' => 0, 'error_text' => 'Not found']); + } + + $fields = $this->db->getFieldNames('investigation'); + $codeField = $this->resolveFirstField($fields, ['Code', 'code']); + $nameField = $this->resolveFirstField($fields, ['Name', 'name']); + + $row = $this->db->table('investigation')->where('id', $invId)->limit(1)->get()->getRowArray(); + if (! $row) { + return $this->response->setJSON(['update' => 0, 'error_text' => 'Not found']); + } + + return $this->response->setJSON([ + 'update' => 1, + 'row' => [ + 'id' => (int) ($row['id'] ?? 0), + 'code' => (string) ($row[$codeField ?? 'code'] ?? ''), + 'name' => (string) ($row[$nameField ?? 'name'] ?? ''), + 'short_name' => (string) ($row['short_name'] ?? ''), + 'category_name' => (string) ($row['category_name'] ?? ''), + 'is_favourite' => (int) ($row['is_favourite'] ?? 0), + 'spec_ids' => (string) ($row['spec_ids'] ?? ''), + ], + 'csrfName' => csrf_token(), + 'csrfHash' => csrf_hash(), + ]); + } + + public function opd_invest_master_save() + { + if (! $this->request->isAJAX()) { + return $this->response->setStatusCode(400)->setJSON(['update' => 0]); + } + if (! $this->db->tableExists('investigation')) { + return $this->response->setJSON(['update' => 0, 'error_text' => 'Investigation table not found']); + } + + $id = (int) $this->request->getPost('id'); + $name = trim((string) $this->request->getPost('name')); + if ($name === '') { + return $this->response->setJSON(['update' => 0, 'error_text' => 'Investigation name is required']); + } + + $fields = $this->db->getFieldNames('investigation'); + $codeField = $this->resolveFirstField($fields, ['Code', 'code']); + $nameField = $this->resolveFirstField($fields, ['Name', 'name']); + if ($codeField === null || $nameField === null) { + return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid table structure']); + } + + $code = trim((string) $this->request->getPost('code')); + if ($code === '') { + $code = strtoupper(preg_replace('/[^A-Z0-9]/i', '', substr($name, 0, 8))) . rand(100, 999); + } + + $data = [ + $nameField => $name, + $codeField => $code, + ]; + foreach (['short_name', 'category_name', 'spec_ids'] as $f) { + if (in_array($f, $fields, true)) { + $data[$f] = trim((string) $this->request->getPost($f)); + } + } + if (in_array('is_favourite', $fields, true)) { + $data['is_favourite'] = ((int) $this->request->getPost('is_favourite')) ? 1 : 0; + } + + if ($id > 0) { + $this->db->table('investigation')->where('id', $id)->update($data); + $savedId = $id; + $message = 'Investigation updated'; + } else { + $this->db->table('investigation')->insert($data); + $savedId = (int) $this->db->insertID(); + $message = 'Investigation added'; + } + + return $this->response->setJSON([ + 'update' => 1, + 'insertid' => $savedId, + 'error_text' => $message, + 'csrfName' => csrf_token(), + 'csrfHash' => csrf_hash(), + ]); + } + + public function opd_invest_master_remove(int $invId) + { + if (! $this->request->isAJAX()) { + return $this->response->setStatusCode(400)->setJSON(['update' => 0]); + } + if ($invId <= 0 || ! $this->db->tableExists('investigation')) { + return $this->response->setJSON(['update' => 0, 'error_text' => 'Not found']); + } + $this->db->table('investigation')->where('id', $invId)->delete(); + return $this->response->setJSON([ + 'update' => 1, + 'error_text' => 'Investigation deleted', + 'csrfName' => csrf_token(), + 'csrfHash' => csrf_hash(), + ]); + } + + public function opd_invest_master_toggle_fav(int $invId) + { + if (! $this->request->isAJAX()) { + return $this->response->setStatusCode(400)->setJSON(['update' => 0]); + } + if ($invId <= 0 || ! $this->db->tableExists('investigation')) { + return $this->response->setJSON(['update' => 0]); + } + $fields = $this->db->getFieldNames('investigation'); + if (! in_array('is_favourite', $fields, true)) { + return $this->response->setJSON(['update' => 0, 'error_text' => 'Run database migration first']); + } + $row = $this->db->table('investigation')->where('id', $invId)->limit(1)->get()->getRowArray(); + if (! $row) { + return $this->response->setJSON(['update' => 0]); + } + $newVal = ((int) ($row['is_favourite'] ?? 0)) === 1 ? 0 : 1; + $this->db->table('investigation')->where('id', $invId)->update(['is_favourite' => $newVal]); + return $this->response->setJSON([ + 'update' => 1, + 'is_favourite' => $newVal, + 'csrfName' => csrf_token(), + 'csrfHash' => csrf_hash(), + ]); + } + + // ───────────────────────────────────────────────────────────────────────── + public function opd_medicince_duplicate_report() { $medicineModel = new OpdMedicineModel($this->db); @@ -3534,29 +3763,59 @@ class Opd_prescription extends BaseController public function investigation_search() { - $q = trim((string) $this->request->getGet('q')); - if ($q === '' || !$this->db->tableExists('investigation')) { + $q = trim((string) $this->request->getGet('q')); + $favOnly = (int) $this->request->getGet('fav_only'); + $specId = (int) $this->request->getGet('spec_id'); + + if (! $this->db->tableExists('investigation')) { return $this->response->setJSON(['rows' => []]); } - $fields = $this->db->getFieldNames('investigation'); - $codeField = in_array('Code', $fields, true) ? 'Code' : (in_array('code', $fields, true) ? 'code' : null); - $nameField = in_array('Name', $fields, true) ? 'Name' : (in_array('name', $fields, true) ? 'name' : null); + $fields = $this->db->getFieldNames('investigation'); + $codeField = in_array('Code', $fields, true) ? 'Code' : (in_array('code', $fields, true) ? 'code' : null); + $nameField = in_array('Name', $fields, true) ? 'Name' : (in_array('name', $fields, true) ? 'name' : null); $shortField = in_array('short_name', $fields, true) ? 'short_name' : null; + $hasFav = in_array('is_favourite', $fields, true); + $hasSpec = in_array('spec_ids', $fields, true); if ($codeField === null || $nameField === null) { return $this->response->setJSON(['rows' => []]); } - $builder = $this->db->table('investigation')->select($codeField . ' as code,' . $nameField . ' as name'); - $builder->groupStart()->like($nameField, $q); - if ($shortField !== null) { - $builder->orLike($shortField, $q); + // When no search term but fav/spec filter applied, still return results + if ($q === '' && $favOnly === 0 && $specId === 0) { + return $this->response->setJSON(['rows' => []]); + } + + $selectCols = $codeField . ' as code, ' . $nameField . ' as name'; + if ($hasFav) { + $selectCols .= ', is_favourite'; + } + + $builder = $this->db->table('investigation')->select($selectCols); + + if ($q !== '') { + $builder->groupStart()->like($nameField, $q); + if ($shortField !== null) { + $builder->orLike($shortField, $q); + } + $builder->groupEnd(); + } + + if ($favOnly && $hasFav) { + $builder->where('is_favourite', 1); + } + + if ($specId > 0 && $hasSpec) { + $builder->where("FIND_IN_SET({$specId}, spec_ids) > 0", null, false); + } + + if ($hasFav) { + $builder->orderBy('is_favourite', 'DESC'); } - $builder->groupEnd(); $builder->orderBy('LOWER(' . $this->db->protectIdentifiers($nameField) . ')', 'ASC', false); - $rows = $builder->limit(20)->get()->getResultArray(); + $rows = $builder->limit(40)->get()->getResultArray(); return $this->response->setJSON(['rows' => $rows]); } diff --git a/app/Database/Migrations/2026-04-01-000031_AddInvestigationMasterFields.php b/app/Database/Migrations/2026-04-01-000031_AddInvestigationMasterFields.php new file mode 100644 index 0000000..ff4d9b6 --- /dev/null +++ b/app/Database/Migrations/2026-04-01-000031_AddInvestigationMasterFields.php @@ -0,0 +1,46 @@ +db->tableExists('investigation')) { + return; + } + + $fields = $this->db->getFieldNames('investigation'); + $add = []; + + if (! in_array('is_favourite', $fields, true)) { + $add['is_favourite'] = ['type' => 'TINYINT', 'constraint' => 1, 'null' => false, 'default' => 0]; + } + if (! in_array('spec_ids', $fields, true)) { + $add['spec_ids'] = ['type' => 'VARCHAR', 'constraint' => 500, 'null' => true, 'default' => null]; + } + if (! in_array('category_name', $fields, true)) { + $add['category_name'] = ['type' => 'VARCHAR', 'constraint' => 100, 'null' => true, 'default' => null]; + } + + if (! empty($add)) { + $this->forge->addColumn('investigation', $add); + } + } + + public function down() + { + if (! $this->db->tableExists('investigation')) { + return; + } + + $fields = $this->db->getFieldNames('investigation'); + foreach (['is_favourite', 'spec_ids', 'category_name'] as $col) { + if (in_array($col, $fields, true)) { + $this->forge->dropColumn('investigation', $col); + } + } + } +} diff --git a/app/Views/billing/opd_invest_master_workspace.php b/app/Views/billing/opd_invest_master_workspace.php new file mode 100644 index 0000000..8266abb --- /dev/null +++ b/app/Views/billing/opd_invest_master_workspace.php @@ -0,0 +1,401 @@ + +
+
+
Investigation Master
+ — +
+ +
+
+ +
+ +
+
+
+ + + + +
+
+
+ + + + + + + + + + + + + + + +
NameCodeShortCategorySpecializationsAction
Loading…
+
+
+
+
+ + +
+
+
New Investigation
+
+ + + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ +
+ +
+ + +
+ + + No specializations found. Add them in Doctor > Specialization. + +
+
+ +
+
+ + +
+
+ +
+ + +
+ +
Ready.
+
+
+
+
+
+ + diff --git a/app/Views/billing/opd_prescription_basic.php b/app/Views/billing/opd_prescription_basic.php index 328996a..2f11872 100644 --- a/app/Views/billing/opd_prescription_basic.php +++ b/app/Views/billing/opd_prescription_basic.php @@ -724,8 +724,17 @@ - - +
+ + + +
+ +
@@ -2003,6 +2012,98 @@ $fallback.removeClass('d-none'); } + var invFavOnly = false; + var invSpecId = 0; + + function loadInvSpecDropdown() { + $.get('', function(data) { + var specs = (data && data.specs) ? data.specs : []; + var $sel = $('#inv_spec_filter'); + $sel.find('option:not(:first)').remove(); + specs.forEach(function(sp) { + $sel.append($('