Investigation Master: CRUD, favourites, specialization filter

This commit is contained in:
DevSoftTech Server 2026-04-01 18:26:33 +05:30
parent e27a69df5d
commit 771fa64778
6 changed files with 854 additions and 14 deletions

View File

@ -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');

View File

@ -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]);
}

View File

@ -0,0 +1,46 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddInvestigationMasterFields extends Migration
{
public function up()
{
if (! $this->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);
}
}
}
}

View File

@ -0,0 +1,401 @@
<?php
/** @var array $med_specs */
$med_specs = $med_specs ?? [];
?>
<section class="container-fluid py-3">
<div class="d-flex align-items-center gap-2 mb-3">
<h5 class="mb-0"><i class="bi bi-clipboard2-pulse me-1"></i> Investigation Master</h5>
<span class="badge bg-secondary" id="inv_total_badge">—</span>
<div class="ms-auto d-flex gap-2">
<button type="button" class="btn btn-outline-secondary btn-sm" id="btn_inv_download_csv">
<i class="bi bi-download"></i> Export CSV
</button>
</div>
</div>
<div class="row g-3">
<!-- ── LIST ──────────────────────────────────────────────────── -->
<div class="col-md-8">
<div class="card">
<div class="card-header d-flex flex-wrap gap-2 align-items-center">
<input type="text" id="inv_filter_text" class="form-control form-control-sm" style="max-width:220px;" placeholder="Search name / code…">
<select id="inv_filter_spec" class="form-select form-select-sm" style="max-width:200px;">
<option value="0">All Specializations</option>
<?php foreach ($med_specs as $sp): ?>
<option value="<?= (int) ($sp->id ?? 0) ?>"><?= esc($sp->SpecName ?? '') ?></option>
<?php endforeach; ?>
</select>
<button type="button" class="btn btn-warning btn-sm" id="btn_inv_filter_fav" title="Show favourites only">
<i class="bi bi-star-fill"></i> Favourites
</button>
<button type="button" class="btn btn-outline-secondary btn-sm d-none" id="btn_inv_filter_all">
Show All
</button>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table id="tbl_invest_master" class="table table-bordered table-sm table-hover mb-0" style="width:100%;">
<thead class="table-light">
<tr>
<th width="32"></th>
<th>Name</th>
<th width="90">Code</th>
<th width="80">Short</th>
<th width="110">Category</th>
<th width="140">Specializations</th>
<th width="100">Action</th>
</tr>
</thead>
<tbody>
<tr><td colspan="7" class="text-muted text-center py-3">Loading…</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- ── FORM ──────────────────────────────────────────────────── -->
<div class="col-md-4">
<div class="card">
<div class="card-header"><strong id="inv_form_title">New Investigation</strong></div>
<div class="card-body">
<?= csrf_field() ?>
<input type="hidden" id="inv_id" value="0">
<div class="mb-2">
<label class="form-label">Name <span class="text-danger">*</span></label>
<input type="text" id="inv_name" class="form-control form-control-sm" placeholder="e.g. CBC, LFT, ECG">
</div>
<div class="mb-2">
<label class="form-label">Code <small class="text-muted">(auto if blank)</small></label>
<input type="text" id="inv_code" class="form-control form-control-sm" placeholder="e.g. CBC01">
</div>
<div class="mb-2">
<label class="form-label">Short Name / Abbreviation</label>
<input type="text" id="inv_short_name" class="form-control form-control-sm" placeholder="e.g. CBC">
</div>
<div class="mb-2">
<label class="form-label">Category</label>
<input type="text" id="inv_category_name" class="form-control form-control-sm" placeholder="e.g. Blood Test, Radiology">
</div>
<div class="mb-2">
<label class="form-label">Specialization(s)</label>
<div id="inv_spec_checkboxes" class="border rounded p-2" style="max-height:140px;overflow-y:auto;">
<?php foreach ($med_specs as $sp): ?>
<div class="form-check form-check-sm">
<input class="form-check-input inv-spec-cb" type="checkbox"
id="inv_spec_<?= (int) ($sp->id ?? 0) ?>"
value="<?= (int) ($sp->id ?? 0) ?>">
<label class="form-check-label small" for="inv_spec_<?= (int) ($sp->id ?? 0) ?>">
<?= esc($sp->SpecName ?? '') ?>
</label>
</div>
<?php endforeach; ?>
<?php if (empty($med_specs)): ?>
<span class="small text-muted">No specializations found. Add them in Doctor &gt; Specialization.</span>
<?php endif; ?>
</div>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="inv_is_favourite" value="1">
<label class="form-check-label" for="inv_is_favourite">
<i class="bi bi-star-fill text-warning"></i> Mark as Favourite
</label>
</div>
</div>
<div class="d-flex gap-2">
<button type="button" class="btn btn-primary btn-sm" id="btn_inv_save">Save</button>
<button type="button" class="btn btn-outline-secondary btn-sm" id="btn_inv_reset">Reset</button>
</div>
<div class="small mt-2" id="inv_msg">Ready.</div>
</div>
</div>
</div>
</div>
</section>
<script>
(function() {
var invDataTable = null;
var favOnlyMode = false;
var filterTimer = null;
/* ── Toast ─────────────────────────────────────────────────────── */
function showToast(message, type) {
var cls = type === 'ok' ? 'alert-success' : (type === 'err' ? 'alert-danger' : 'alert-info');
var id = 'inv_toast_' + Date.now();
var html = '<div id="' + id + '" class="alert ' + cls + ' shadow-sm d-flex align-items-start justify-content-between"'
+ ' role="alert" style="position:fixed;top:20px;right:20px;z-index:9999;min-width:260px;max-width:420px;gap:12px;">'
+ '<div>' + $('<div>').text(message || '').html() + '</div>'
+ '<button type="button" class="btn-close btn-sm" data-tid="' + id + '"></button></div>';
$('body').append(html);
setTimeout(function() { $('#' + id).fadeOut(200, function() { $(this).remove(); }); }, 4000);
}
$(document).on('click', '.btn-close[data-tid]', function() {
$('#' + $(this).data('tid')).fadeOut(150, function() { $(this).remove(); });
});
/* ── CSRF ───────────────────────────────────────────────────────── */
function getCsrfPair() {
var input = document.querySelector('input[name="<?= csrf_token() ?>"]');
return input
? { name: input.getAttribute('name'), value: input.value }
: { name: '<?= csrf_token() ?>', value: '<?= csrf_hash() ?>' };
}
function updateCsrf(data) {
if (!data || !data.csrfName) return;
var input = document.querySelector('input[name="' + data.csrfName + '"]');
if (input) input.value = data.csrfHash || '';
}
function apiPost(url, payload, cb) {
var csrf = getCsrfPair();
payload = payload || {};
payload[csrf.name] = csrf.value;
$.post(url, payload, function(data) { updateCsrf(data); cb(data || {}); }, 'json');
}
function apiGet(url, cb) {
$.get(url, function(data) { cb(data || {}); }, 'json');
}
/* ── Form helpers ───────────────────────────────────────────────── */
function setMsg(type, text) {
var $m = $('#inv_msg');
$m.removeClass('text-success text-danger text-muted');
$m.addClass(type === 'ok' ? 'text-success' : type === 'err' ? 'text-danger' : 'text-muted');
$m.text(text || '');
}
function getCheckedSpecIds() {
var ids = [];
$('.inv-spec-cb:checked').each(function() { ids.push($(this).val()); });
return ids.join(',');
}
function clearForm() {
$('#inv_id').val('0');
$('#inv_name,#inv_code,#inv_short_name,#inv_category_name').val('');
$('.inv-spec-cb').prop('checked', false);
$('#inv_is_favourite').prop('checked', false);
$('#inv_form_title').text('New Investigation');
setMsg('normal', 'Ready.');
}
function fillForm(row) {
row = row || {};
$('#inv_id').val(row.id || 0);
$('#inv_name').val(row.name || '');
$('#inv_code').val(row.code || '');
$('#inv_short_name').val(row.short_name || '');
$('#inv_category_name').val(row.category_name || '');
$('#inv_is_favourite').prop('checked', parseInt(row.is_favourite || 0) === 1);
$('.inv-spec-cb').prop('checked', false);
var specIds = (row.spec_ids || '').split(',').map(function(s) { return s.trim(); }).filter(Boolean);
specIds.forEach(function(sid) {
$('#inv_spec_' + sid).prop('checked', true);
});
$('#inv_form_title').text('Edit: ' + (row.name || ''));
}
function buildPayload() {
return {
id: parseInt($('#inv_id').val() || '0', 10),
name: ($('#inv_name').val() || '').trim(),
code: ($('#inv_code').val() || '').trim(),
short_name: ($('#inv_short_name').val() || '').trim(),
category_name: ($('#inv_category_name').val() || '').trim(),
spec_ids: getCheckedSpecIds(),
is_favourite: $('#inv_is_favourite').is(':checked') ? 1 : 0
};
}
/* ── DataTable ──────────────────────────────────────────────────── */
function getAjaxParams() {
return {
filter: ($('#inv_filter_text').val() || '').trim(),
spec_id: parseInt($('#inv_filter_spec').val() || '0', 10),
fav_only: favOnlyMode ? 1 : 0
};
}
function initTable() {
if (invDataTable) return;
if (!window.jQuery || typeof $.fn.DataTable !== 'function') return;
invDataTable = $('#tbl_invest_master').DataTable({
serverSide: true,
processing: true,
searching: false,
paging: true,
pageLength: 25,
lengthChange: false,
ajax: {
url: '<?= base_url('Opd_prescription/opd_invest_master_data') ?>',
type: 'GET',
data: function(d) {
var p = getAjaxParams();
d.filter = p.filter;
d.spec_id = p.spec_id;
d.fav_only = p.fav_only;
}
},
columns: [
{
data: 'is_favourite', orderable: false, className: 'text-center',
render: function(val, type, row) {
var isFav = parseInt(val || 0) === 1;
return '<button class="btn btn-link p-0 btn-toggle-fav" data-id="' + (row.id || 0) + '" '
+ 'title="' + (isFav ? 'Remove from favourites' : 'Mark as favourite') + '">'
+ '<i class="bi bi-star' + (isFav ? '-fill text-warning' : ' text-muted') + '"></i></button>';
}
},
{ data: 'name' },
{ data: 'code', orderable: false },
{ data: 'short_name', defaultContent: '', orderable: false },
{ data: 'category_name', defaultContent: '', orderable: false },
{
data: 'spec_ids', orderable: false,
render: function(val) {
if (!val) return '<span class="text-muted small">—</span>';
return '<span class="small">' + $('<div>').text(val).html() + '</span>';
}
},
{
data: 'id', orderable: false,
render: function(id, type, row) {
return '<button class="btn btn-outline-primary btn-sm btn-inv-edit" data-id="' + id + '">Edit</button> '
+ '<button class="btn btn-outline-danger btn-sm btn-inv-del" data-id="' + id + '">Del</button>';
}
}
],
drawCallback: function(settings) {
var info = settings.json || {};
$('#inv_total_badge').text((info.recordsTotal || 0) + ' total');
},
language: { processing: 'Loading…', zeroRecords: 'No investigations found.' }
});
}
function reloadTable() {
if (!invDataTable) { initTable(); return; }
invDataTable.ajax.reload(null, true);
}
/* ── Save ───────────────────────────────────────────────────────── */
function saveInvestigation(payload) {
if (!payload.name) { setMsg('err', 'Name is required.'); return; }
apiPost('<?= base_url('Opd_prescription/opd_invest_master_save') ?>', payload, function(data) {
if (parseInt(data.update || 0) !== 1) {
setMsg('err', data.error_text || 'Unable to save');
return;
}
$('#inv_id').val(data.insertid || 0);
setMsg('ok', data.error_text || 'Saved');
showToast(data.error_text || 'Saved', 'ok');
reloadTable();
});
}
/* ── Event bindings ─────────────────────────────────────────────── */
$('#btn_inv_save').on('click', function() {
saveInvestigation(buildPayload());
});
$('#btn_inv_reset').on('click', function() {
clearForm();
});
// Filter text / spec — debounced
$('#inv_filter_text, #inv_filter_spec').on('input change', function() {
clearTimeout(filterTimer);
filterTimer = setTimeout(reloadTable, 350);
});
// Fav toggle filter
$('#btn_inv_filter_fav').on('click', function() {
favOnlyMode = true;
$(this).addClass('d-none');
$('#btn_inv_filter_all').removeClass('d-none');
reloadTable();
});
$('#btn_inv_filter_all').on('click', function() {
favOnlyMode = false;
$(this).addClass('d-none');
$('#btn_inv_filter_fav').removeClass('d-none');
reloadTable();
});
// Row edit
$(document).on('click', '.btn-inv-edit', function() {
var id = parseInt($(this).data('id') || '0', 10);
if (id <= 0) return;
apiGet('<?= base_url('Opd_prescription/opd_invest_master_get') ?>/' + id, function(data) {
if (parseInt(data.update || 0) !== 1) { setMsg('err', data.error_text || 'Cannot load'); return; }
fillForm(data.row || {});
setMsg('normal', 'Loaded for edit.');
$('html, body').animate({ scrollTop: $('#inv_name').offset().top - 80 }, 200);
});
});
// Row delete
$(document).on('click', '.btn-inv-del', function() {
var id = parseInt($(this).data('id') || '0', 10);
if (id <= 0) return;
if (!confirm('Delete this investigation from master?')) return;
apiPost('<?= base_url('Opd_prescription/opd_invest_master_remove') ?>/' + id, {}, function(data) {
if (parseInt(data.update || 0) !== 1) { showToast(data.error_text || 'Error', 'err'); return; }
showToast('Deleted', 'ok');
reloadTable();
if (parseInt($('#inv_id').val() || '0', 10) === id) clearForm();
});
});
// Row fav toggle
$(document).on('click', '.btn-toggle-fav', function() {
var id = parseInt($(this).data('id') || '0', 10);
if (id <= 0) return;
apiPost('<?= base_url('Opd_prescription/opd_invest_master_toggle_fav') ?>/' + id, {}, function(data) {
if (parseInt(data.update || 0) !== 1) { showToast(data.error_text || 'Error', 'err'); return; }
reloadTable();
// sync form star if editing this row
if (parseInt($('#inv_id').val() || '0', 10) === id) {
$('#inv_is_favourite').prop('checked', data.is_favourite === 1);
}
});
});
// CSV export — simple GET with current filter params
$('#btn_inv_download_csv').on('click', function() {
var p = getAjaxParams();
var url = '<?= base_url('Opd_prescription/opd_invest_master_data') ?>?length=9999&start=0'
+ '&filter=' + encodeURIComponent(p.filter)
+ '&spec_id=' + p.spec_id
+ '&fav_only=' + p.fav_only;
$.get(url, function(data) {
var rows = (data && data.data) ? data.data : [];
if (!rows.length) { showToast('No data to export', 'err'); return; }
var csv = 'Name,Code,Short Name,Category,Specialization IDs,Favourite\n';
rows.forEach(function(r) {
csv += [r.name, r.code, r.short_name || '', r.category_name || '', r.spec_ids || '', r.is_favourite || 0]
.map(function(v) { return '"' + String(v).replace(/"/g, '""') + '"'; })
.join(',') + '\n';
});
var blob = new Blob([csv], { type: 'text/csv' });
var a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'investigation_master.csv';
a.click();
}, 'json');
});
/* ── Init ───────────────────────────────────────────────────────── */
$(function() {
initTable();
});
})();
</script>

View File

@ -724,8 +724,17 @@
<select class="form-select" id="investigation_name_select2" style="width:100%;">
<option value="">Search investigation...</option>
</select>
<input type="text" class="form-control d-none mt-2" id="investigation_name" list="investigation_suggest" placeholder="Type investigation name">
<datalist id="investigation_suggest"></datalist>
<div class="d-flex flex-wrap gap-1 mt-1 align-items-center" id="inv_quick_filter_bar">
<button type="button" class="btn btn-outline-warning btn-sm px-2 py-0" id="btn_inv_fav_toggle" title="Search favourites only">
<i class="bi bi-star"></i> Fav
</button>
<select id="inv_spec_filter" class="form-select form-select-sm py-0" style="max-width:160px;height:26px;">
<option value="0">All Specs</option>
</select>
<span class="small text-muted" id="inv_filter_hint" style="display:none;">Filter active — type anything to search</span>
</div>
<input type="text" class="form-control d-none mt-2" id="investigation_name" list="investigation_suggest" placeholder="Type investigation name">
<datalist id="investigation_suggest"></datalist>
</div>
<div class="col-md-2 d-grid gap-1">
<button type="button" class="btn btn-primary" id="btn_add_investigation">Add</button>
@ -2003,6 +2012,98 @@
$fallback.removeClass('d-none');
}
var invFavOnly = false;
var invSpecId = 0;
function loadInvSpecDropdown() {
$.get('<?= base_url('Opd_prescription/opd_invest_master_specs') ?>', 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($('<option>').val(sp.id).text(sp.SpecName));
});
}, 'json');
}
function updateInvFilterHint() {
var active = invFavOnly || invSpecId > 0;
$('#inv_filter_hint').toggle(active);
$('#btn_inv_fav_toggle').toggleClass('btn-warning', invFavOnly).toggleClass('btn-outline-warning', !invFavOnly);
}
var invFavOnly = false;
var invSpecId = 0;
function loadInvSpecDropdown() {
$.get('<?= base_url('Opd_prescription/opd_invest_master_specs') ?>', 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($('<option>').val(sp.id).text(sp.SpecName));
});
}, 'json');
}
function updateInvFilterHint() {
var active = invFavOnly || invSpecId > 0;
$('#inv_filter_hint').toggle(active);
$('#btn_inv_fav_toggle')
.toggleClass('btn-warning', invFavOnly)
.toggleClass('btn-outline-warning', !invFavOnly)
.find('i').toggleClass('bi-star-fill', invFavOnly).toggleClass('bi-star', !invFavOnly);
}
function initInvestigationPicker() {
var $select = $('#investigation_name_select2');
var $fallback = $('#investigation_name');
if (!$select.length) {
return;
}
if ($.fn && $.fn.select2) {
$select.select2({
width: '100%',
placeholder: 'Search investigation...',
allowClear: true,
ajax: {
delay: 250,
transport: function(params, success, failure) {
var term = (params.data && params.data.term) ? params.data.term : '';
$.ajax({
url: '<?= base_url('Opd_prescription/investigation_search') ?>',
dataType: 'json',
data: { q: term, fav_only: invFavOnly ? 1 : 0, spec_id: invSpecId },
success: success,
error: failure
});
},
processResults: function(data) {
var rows = (data && data.rows) ? data.rows : [];
return {
results: rows.map(function(row) {
var name = (row.name || '').toString();
var code = (row.code || '').toString();
var isFav = parseInt(row.is_favourite || 0) === 1;
var label = (isFav ? '★ ' : '') + name + (code ? ' [' + code + ']' : '');
return {
id: name + (code ? ' [' + code + ']' : ''),
text: label,
name: name,
code: code
};
})
};
}
}
});
$fallback.addClass('d-none');
return;
}
$fallback.removeClass('d-none');
}
function setStatus(type, message) {
var $badge = $('#rx_status_badge');
@ -4024,7 +4125,26 @@
refreshCustomProfileSelect();
loadInvestigationShortcutsFromLegacy();
initInvestigationPicker();
loadMedicineDoseMasters();
loadInvSpecDropdown();
$('#btn_inv_fav_toggle').on('click', function() {
invFavOnly = !invFavOnly;
updateInvFilterHint();
// Trigger Select2 to re-search with current term
if ($.fn && $.fn.select2 && $('#investigation_name_select2').length) {
$('#investigation_name_select2').trigger('input.select2');
}
});
$('#inv_spec_filter').on('change', function() {
invSpecId = parseInt($(this).val() || '0', 10);
updateInvFilterHint();
if ($.fn && $.fn.select2 && $('#investigation_name_select2').length) {
$('#investigation_name_select2').trigger('input.select2');
}
});
loadMedicineDoseMasters();
$('#advise_investigation_notes').val($('#investigation').val() || '');
$(document).on('click', '.btn-del-invest', function() {

View File

@ -240,6 +240,13 @@ if (! $canDiagnosis && $user && method_exists($user, 'inGroup')) {
</li>
<li class="nav-item">
<a class="nav-link collapsed" href="javascript:load_form('<?= base_url('Opd_prescription/opd_advice') ?>','OPD Advice Master')">
<a class="nav-link collapsed" href="javascript:load_form('<?= base_url('Opd_prescription/opd_invest_master') ?>','Investigation Master')">
<i class="bi bi-clipboard2-pulse"></i>
<span>Investigation Master</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link collapsed" href="javascript:load_form('<?= base_url('Opd_prescription/opd_advice') ?>','OPD Advice Master')">
<i class="bi bi-chat-left-text"></i>
<span>OPD Advice Master</span>
</a>