Investigation Master: CRUD, favourites, specialization filter
This commit is contained in:
parent
e27a69df5d
commit
771fa64778
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
401
app/Views/billing/opd_invest_master_workspace.php
Normal file
401
app/Views/billing/opd_invest_master_workspace.php
Normal 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 > 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>
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user