Add 'Save in Master' quick-add for Course/Treatment section

FEATURE: Quick-add functionality for Course/Treatment master terms

NEW COMPONENTS:
1. Course master table: ipd_discharge_course_master
   - term_name, term_code, icd_code, is_active
   - Auto-creates on first use
2. Controller endpoints in Ipd_discharge.php:
   - course_master_lookup() - AJAX autocomplete search
   - course_master_list() - Full list with filters
   - course_master_save() - Create/update records
   - course_master_delete() - Remove records
3. Routes added to Config/Routes.php:
   - Ipd_discharge/course_master_lookup (GET)
   - Ipd_discharge/course_master_list (GET)
   - Ipd_discharge/course_master_save (POST)
   - Ipd_discharge/course_master_delete (POST)

VIEW CHANGES (discharge_create.php):
- Course input now uses input-group layout
- Green 'Save in Master' button added
- Custom dropdown for autocomplete with code badges
- Hidden field: new_course_master_id
- JavaScript: initCourseAutocomplete() function
- Integrated with existing quickAddTermModal

QUICK-ADD MODAL ENHANCEMENT:
- Now supports 3 types: surgery, procedure, course
- Dynamic endpoint selection based on type
- Course type uses course_master_save endpoint
- Auto-populates course input and master_id on save

AUTOCOMPLETE FEATURES:
- Type 2+ characters → searches course_master table
- Dropdown shows: Name + SNOMED/ICD code badges
- Arrow key navigation (Up/Down/Enter/Escape)
- Click or Enter to select
- Hover highlighting with visual feedback
- 300ms debounce for performance

WORKFLOW:
1. Type course/treatment name
2. See dropdown with existing terms (if any)
3. Click 'Save in Master' to add new term
4. Modal opens with name pre-filled
5. Enter SNOMED/ICD codes (optional)
6. Save → Auto-populates input + master_id
7. Click '+ADD Row' to add to discharge

BENEFITS:
- Consistent UI across Surgery/Procedure/Course sections
- Fast term entry without leaving form
- Master library builds during workflow
- ABDM compliance with code support
- Reusable terms across discharges
This commit is contained in:
DevSoftTech Server 2026-06-06 06:53:27 +05:30
parent b203dcac98
commit 38e06ed1d7
3 changed files with 402 additions and 5 deletions

View File

@ -375,6 +375,10 @@ $routes->get('Ipd_discharge/surgery_master_lookup', 'Ipd_discharge::surgery_mast
$routes->get('Ipd_discharge/surgery_master_list', 'Ipd_discharge::surgery_master_list');
$routes->post('Ipd_discharge/surgery_master_save', 'Ipd_discharge::surgery_master_save');
$routes->post('Ipd_discharge/surgery_master_delete', 'Ipd_discharge::surgery_master_delete');
$routes->get('Ipd_discharge/course_master_lookup', 'Ipd_discharge::course_master_lookup');
$routes->get('Ipd_discharge/course_master_list', 'Ipd_discharge::course_master_list');
$routes->post('Ipd_discharge/course_master_save', 'Ipd_discharge::course_master_save');
$routes->post('Ipd_discharge/course_master_delete', 'Ipd_discharge::course_master_delete');
$routes->get('Ipd_discharge/diagnosis_icd_lookup', 'Ipd_discharge::diagnosis_icd_lookup');
$routes->post('Ipd_discharge/diagnosis_icd_seed_starter', 'Ipd_discharge::diagnosis_icd_seed_starter');
$routes->post('Ipd_discharge/update_complaint_field', 'Ipd_discharge::update_complaint_field');

View File

@ -2631,6 +2631,218 @@ class Ipd_discharge extends BaseController
]);
}
// Course/Treatment Master CRUD
private function ensureDischargeCourseMasterTable(): bool
{
if ($this->db->tableExists('ipd_discharge_course_master')) {
return true;
}
try {
$sql = "CREATE TABLE IF NOT EXISTS ipd_discharge_course_master (
id INT AUTO_INCREMENT PRIMARY KEY,
term_name VARCHAR(255) NOT NULL,
term_code VARCHAR(60) DEFAULT NULL,
icd_code VARCHAR(60) DEFAULT NULL,
is_active TINYINT(1) NOT NULL DEFAULT 1,
created_at DATETIME NULL,
updated_at DATETIME NULL,
UNIQUE KEY uniq_term_name (term_name),
INDEX idx_active_name (is_active, term_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4";
$this->db->query($sql);
} catch (\Throwable $e) {
return false;
}
return $this->db->tableExists('ipd_discharge_course_master');
}
public function course_master_lookup()
{
$q = trim((string) $this->request->getGet('q'));
$rows = [];
if ($q !== '') {
if ($this->ensureDischargeCourseMasterTable()) {
$dbRows = $this->db->table('ipd_discharge_course_master')
->select('id, term_name, term_code, icd_code')
->where('is_active', 1)
->groupStart()
->like('term_name', $q)
->orLike('term_code', $q)
->orLike('icd_code', $q)
->groupEnd()
->orderBy('term_name', 'ASC')
->limit(20)
->get()
->getResultArray();
foreach ($dbRows as $row) {
$rows[] = [
'id' => (int) ($row['id'] ?? 0),
'term_name' => (string) ($row['term_name'] ?? ''),
'term_code' => (string) ($row['term_code'] ?? ''),
'icd_code' => (string) ($row['icd_code'] ?? ''),
];
}
}
}
return $this->response->setJSON(['rows' => $rows]);
}
public function course_master_list()
{
$q = trim((string) $this->request->getGet('q'));
$rows = [];
if (! $this->ensureDischargeCourseMasterTable()) {
return $this->response->setJSON(['rows' => []]);
}
$builder = $this->db->table('ipd_discharge_course_master')
->select('id, term_name, term_code, icd_code, is_active');
if ($q !== '') {
$builder->groupStart()
->like('term_name', $q)
->orLike('term_code', $q)
->orLike('icd_code', $q)
->groupEnd();
}
$dbRows = $builder->orderBy('term_name', 'ASC')
->limit(100)
->get()
->getResultArray();
foreach ($dbRows as $row) {
$rows[] = [
'id' => (int) ($row['id'] ?? 0),
'term_name' => (string) ($row['term_name'] ?? ''),
'term_code' => (string) ($row['term_code'] ?? ''),
'icd_code' => (string) ($row['icd_code'] ?? ''),
'is_active' => (int) ($row['is_active'] ?? 1),
];
}
return $this->response->setJSON(['rows' => $rows]);
}
public function course_master_save()
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
}
if (! $this->ensureDischargeCourseMasterTable()) {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'Unable to access course master table',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
$id = (int) $this->request->getPost('id');
$name = trim((string) $this->request->getPost('name'));
$code = trim((string) $this->request->getPost('code'));
$icdCode = trim((string) $this->request->getPost('icd_code'));
$isActive = (int) $this->request->getPost('is_active') === 0 ? 0 : 1;
if ($name === '') {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'Name is required',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
$now = date('Y-m-d H:i:s');
$payload = [
'term_name' => $name,
'term_code' => $code !== '' ? $code : null,
'icd_code' => $icdCode !== '' ? $icdCode : null,
'is_active' => $isActive,
'updated_at' => $now,
];
try {
$table = $this->db->table('ipd_discharge_course_master');
if ($id > 0) {
$ok = (bool) $table->where('id', $id)->update($payload);
} else {
$payload['created_at'] = $now;
$ok = (bool) $table->insert($payload);
$id = (int) ($this->db->insertID() ?: 0);
}
if (! $ok) {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'Unable to save record',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
return $this->response->setJSON([
'update' => 1,
'id' => $id,
'error_text' => 'Master record saved',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
} catch (\Throwable $e) {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'Save failed: ' . $e->getMessage(),
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
}
public function course_master_delete()
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
}
if (! $this->ensureDischargeCourseMasterTable()) {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'Unable to access course master table',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
$id = (int) $this->request->getPost('id');
if ($id <= 0) {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'Invalid record id',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
$ok = (bool) $this->db->table('ipd_discharge_course_master')
->where('id', $id)
->delete();
return $this->response->setJSON([
'update' => $ok ? 1 : 0,
'error_text' => $ok ? 'Record deleted' : 'Unable to delete record',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function diagnosis_icd_lookup()
{
$q = trim((string) $this->request->getGet('q'));

View File

@ -1165,10 +1165,19 @@ $allergyStatusNoKnown = in_array($allergyStatusNormalized, ['no known drug aller
</tbody>
</table>
<input type="hidden" name="course_remove_id" id="course_remove_id" value="0">
<input type="hidden" name="new_course_master_id" id="new_course_master_id" value="0">
<div class="row g-2">
<div class="col-md-6"><input type="text" class="form-control" name="new_course_name" id="new_course_name" list="discharge_course_suggest" autocomplete="off" placeholder="Course / treatment"></div>
<div class="col-md-5"><input type="text" class="form-control" name="new_course_remark" placeholder="Remark"></div>
<div class="col-md-1"><button type="submit" class="btn btn-primary btn-sm" name="action" value="add_course">+ADD</button></div>
<div class="col-md-6 position-relative">
<div class="input-group">
<input type="text" class="form-control" name="new_course_name" id="new_course_name" autocomplete="off" placeholder="Course / treatment (type to search master)">
<button type="button" class="btn btn-outline-success btn-sm" id="btn_quick_add_course" title="Save new term in master">
<i class="bi bi-plus-circle"></i> Save in Master
</button>
</div>
<div id="discharge_course_dropdown" class="dropdown-menu" style="display:none;position:absolute;z-index:1050;max-height:250px;overflow-y:auto;width:100%;"></div>
</div>
<div class="col-md-4"><input type="text" class="form-control" name="new_course_remark" placeholder="Remark"></div>
<div class="col-md-2"><button type="submit" class="btn btn-primary btn-sm w-100" name="action" value="add_course">+ADD Row</button></div>
</div>
<datalist id="discharge_course_suggest"></datalist>
<div id="discharge_course_status" class="complaint-status text-muted"></div>
@ -2247,18 +2256,28 @@ $allergyStatusNoKnown = in_array($allergyStatusNormalized, ['no known drug aller
var csrf = getCsrfPair(form);
var payload = {
id: 0,
type: type,
name: name,
code: code,
icd_code: icd,
is_active: 1
};
// For surgery/procedure, include type in payload
if (type === 'surgery' || type === 'procedure') {
payload.type = type;
}
payload[csrf.name] = csrf.value;
setQuickStatus('Saving...', 'muted');
btnSave.disabled = true;
$.post('<?= base_url('Ipd_discharge/surgery_master_save') ?>', payload, function(data) {
// Use appropriate endpoint based on type
var saveUrl = type === 'course'
? '<?= base_url('Ipd_discharge/course_master_save') ?>'
: '<?= base_url('Ipd_discharge/surgery_master_save') ?>';
$.post(saveUrl, payload, function(data) {
updateFormCsrf(form, data);
btnSave.disabled = false;
@ -2281,6 +2300,11 @@ $allergyStatusNoKnown = in_array($allergyStatusNormalized, ['no known drug aller
var procedureMasterId = document.getElementById('new_procedure_master_id');
if (procedureInput) procedureInput.value = name;
if (procedureMasterId) procedureMasterId.value = savedId;
} else if (type === 'course') {
var courseInput = document.getElementById('new_course_name');
var courseMasterId = document.getElementById('new_course_master_id');
if (courseInput) courseInput.value = name;
if (courseMasterId) courseMasterId.value = savedId;
}
// Close modal after short delay
@ -2470,6 +2494,162 @@ $allergyStatusNoKnown = in_array($allergyStatusNormalized, ['no known drug aller
}
}
// Course/Treatment autocomplete with SNOMED/ICD code display
function initCourseAutocomplete() {
var input = document.getElementById('new_course_name');
var dropdown = document.getElementById('discharge_course_dropdown');
var hidden = document.getElementById('new_course_master_id');
if (!input || !dropdown || !hidden) return;
var searchTimer = null;
var highlightedIndex = -1;
var currentResults = [];
input.addEventListener('input', function() {
var q = input.value.trim();
highlightedIndex = -1;
if (q.length < 2) {
dropdown.style.display = 'none';
return;
}
clearTimeout(searchTimer);
searchTimer = setTimeout(function() {
if (!window.jQuery) return;
$.get('<?= base_url('Ipd_discharge/course_master_lookup') ?>?q=' + encodeURIComponent(q), function(data) {
currentResults = (data && data.rows) ? data.rows : [];
if (!currentResults.length) {
dropdown.style.display = 'none';
return;
}
var html = '';
currentResults.forEach(function(row) {
var name = row.term_name || '';
var code = row.term_code || '';
var icd = row.icd_code || '';
var codeDisplay = '';
if (code && icd) {
codeDisplay = '<span class="badge bg-info text-dark me-1">' + code + '</span><span class="badge bg-secondary">' + icd + '</span>';
} else if (code) {
codeDisplay = '<span class="badge bg-info text-dark">' + code + '</span>';
} else if (icd) {
codeDisplay = '<span class="badge bg-secondary">' + icd + '</span>';
}
html += '<div class="dropdown-item px-2 py-2" data-id="' + (row.id || 0) + '" data-name="' + name.replace(/"/g, '&quot;') + '" style="cursor:pointer;font-size:.9rem;border-bottom:1px solid #f0f0f0;">';
html += '<div class="fw-semibold">' + name + '</div>';
if (codeDisplay) {
html += '<div class="small mt-1">' + codeDisplay + '</div>';
}
html += '</div>';
});
dropdown.innerHTML = html;
dropdown.style.display = 'block';
dropdown.querySelectorAll('.dropdown-item').forEach(function(item, idx) {
item.addEventListener('mouseenter', function() {
dropdown.querySelectorAll('.dropdown-item').forEach(function(el) {
el.style.backgroundColor = '';
});
this.style.backgroundColor = '#f8f9fa';
highlightedIndex = idx;
});
item.addEventListener('mouseleave', function() {
this.style.backgroundColor = '';
});
item.addEventListener('click', function() {
var id = this.getAttribute('data-id');
var name = this.getAttribute('data-name');
input.value = name;
hidden.value = id;
dropdown.style.display = 'none';
});
});
}, 'json');
}, 300);
});
input.addEventListener('keydown', function(e) {
var items = dropdown.querySelectorAll('.dropdown-item');
var isVisible = dropdown.style.display === 'block' && items.length > 0;
if (e.key === 'ArrowDown' && isVisible) {
e.preventDefault();
highlightedIndex = Math.min(highlightedIndex + 1, items.length - 1);
updateHighlight(items);
} else if (e.key === 'ArrowUp' && isVisible) {
e.preventDefault();
highlightedIndex = Math.max(highlightedIndex - 1, 0);
updateHighlight(items);
} else if (e.key === 'Enter' && isVisible) {
e.preventDefault();
if (highlightedIndex >= 0 && items[highlightedIndex]) {
items[highlightedIndex].click();
}
} else if (e.key === 'Escape') {
dropdown.style.display = 'none';
highlightedIndex = -1;
}
});
input.addEventListener('blur', function() {
setTimeout(function() {
dropdown.style.display = 'none';
}, 200);
});
function updateHighlight(items) {
items.forEach(function(item, idx) {
if (idx === highlightedIndex) {
item.style.backgroundColor = '#007bff';
item.style.color = '#fff';
item.scrollIntoView({ block: 'nearest' });
} else {
item.style.backgroundColor = '';
item.style.color = '';
}
});
}
// Quick-add button handler
var btnQuickCourse = document.getElementById('btn_quick_add_course');
if (btnQuickCourse) {
btnQuickCourse.addEventListener('click', function() {
var courseName = input.value.trim();
if (!courseName) {
alert('Please enter a course/treatment name first');
return;
}
var quickModal = document.getElementById('quickAddTermModal');
var quickType = document.getElementById('quick_term_type');
var quickTypeLabel = document.getElementById('quick_term_type_label');
var quickName = document.getElementById('quick_term_name');
var quickCode = document.getElementById('quick_term_code');
var quickIcd = document.getElementById('quick_term_icd');
var quickStatus = document.getElementById('quick_term_status');
if (!quickModal || !quickName) return;
quickType.value = 'course';
quickTypeLabel.textContent = 'Course/Treatment';
quickName.value = courseName;
quickCode.value = '';
quickIcd.value = '';
quickStatus.textContent = '';
quickStatus.className = 'text-muted';
showModalById('quickAddTermModal');
});
}
}
function showModalById(modalId) {
var el = document.getElementById(modalId);
if (!el) {
@ -3837,6 +4017,7 @@ $allergyStatusNoKnown = in_array($allergyStatusNormalized, ['no known drug aller
initComplaintTools();
initSurgeryProcedureAutocomplete();
initSurgeryTools(getDischargeForm());
initCourseAutocomplete();
initDiagnosisTools();
initCourseTools();
initMedicineTools();