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:
parent
b203dcac98
commit
38e06ed1d7
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
|
|
|
|||
|
|
@ -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, '"') + '" 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();
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user