Add OPD keyword manager and caret-aligned autocomplete
This commit is contained in:
parent
418fb3e5e7
commit
7275d13500
|
|
@ -535,6 +535,9 @@ $routes->get('Opd_prescription/get_complaints', 'Opd_prescription::get_complaint
|
|||
$routes->get('Opd_prescription/get_disease', 'Opd_prescription::get_disease');
|
||||
$routes->get('Opd_prescription/get_finding_exam', 'Opd_prescription::get_finding_exam');
|
||||
$routes->post('Opd_prescription/autotype_keyword_save', 'Opd_prescription::autotype_keyword_save');
|
||||
$routes->get('Opd_prescription/autotype_keyword_list', 'Opd_prescription::autotype_keyword_list');
|
||||
$routes->post('Opd_prescription/autotype_keyword_update', 'Opd_prescription::autotype_keyword_update');
|
||||
$routes->post('Opd_prescription/autotype_keyword_remove', 'Opd_prescription::autotype_keyword_remove');
|
||||
$routes->post('Opd_prescription/complaints_parse', 'Opd_prescription::complaints_parse');
|
||||
$routes->post('Opd_prescription/complaints_ai_draft', 'Opd_prescription::complaints_ai_draft');
|
||||
$routes->post('Opd_prescription/clinical_autotype', 'Opd_prescription::clinical_autotype');
|
||||
|
|
|
|||
|
|
@ -4111,6 +4111,195 @@ class Opd_prescription extends BaseController
|
|||
]);
|
||||
}
|
||||
|
||||
public function autotype_keyword_list()
|
||||
{
|
||||
if (! $this->canAccessPrescription()) {
|
||||
return $this->response->setStatusCode(403)->setJSON(['rows' => []]);
|
||||
}
|
||||
|
||||
$section = trim((string) $this->request->getGet('section'));
|
||||
$scope = strtolower(trim((string) $this->request->getGet('scope')));
|
||||
$q = trim((string) $this->request->getGet('q'));
|
||||
$docId = max(0, (int) ($this->getCurrentUserId() ?? 0));
|
||||
$canMaster = $this->canManageMasterKeyword();
|
||||
|
||||
if ($scope === '') {
|
||||
$scope = 'all';
|
||||
}
|
||||
|
||||
if (! $this->ensureAutotypeKeywordTable()) {
|
||||
return $this->response->setJSON(['rows' => []]);
|
||||
}
|
||||
|
||||
$builder = $this->db->table('opd_autotype_keyword_master')
|
||||
->select('id,doc_id,section_key,keyword,updated_at')
|
||||
->where('is_active', 1);
|
||||
|
||||
if ($this->isAutotypeKeywordSectionAllowed($section)) {
|
||||
$builder->where('section_key', $section);
|
||||
}
|
||||
|
||||
if ($scope === 'master') {
|
||||
$builder->where('doc_id', 0);
|
||||
} elseif ($scope === 'doctor') {
|
||||
$builder->where('doc_id', $docId);
|
||||
} else {
|
||||
$builder->groupStart()
|
||||
->where('doc_id', 0)
|
||||
->orWhere('doc_id', $docId)
|
||||
->groupEnd();
|
||||
}
|
||||
|
||||
if ($q !== '') {
|
||||
$builder->like('keyword', $q);
|
||||
}
|
||||
|
||||
$rows = $builder
|
||||
->orderBy('doc_id', 'DESC')
|
||||
->orderBy('updated_at', 'DESC')
|
||||
->orderBy('keyword', 'ASC')
|
||||
->limit(200)
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
$out = [];
|
||||
foreach ($rows as $row) {
|
||||
$id = (int) ($row['id'] ?? 0);
|
||||
$rowDocId = (int) ($row['doc_id'] ?? 0);
|
||||
$isMaster = $rowDocId === 0;
|
||||
$canEdit = $isMaster ? $canMaster : ($rowDocId === $docId);
|
||||
|
||||
$out[] = [
|
||||
'id' => $id,
|
||||
'doc_id' => $rowDocId,
|
||||
'section_key' => (string) ($row['section_key'] ?? ''),
|
||||
'keyword' => (string) ($row['keyword'] ?? ''),
|
||||
'scope' => $isMaster ? 'master' : 'doctor',
|
||||
'scope_label' => $isMaster ? 'Master' : 'My',
|
||||
'updated_at' => (string) ($row['updated_at'] ?? ''),
|
||||
'can_edit' => $canEdit ? 1 : 0,
|
||||
'can_delete' => $canEdit ? 1 : 0,
|
||||
];
|
||||
}
|
||||
|
||||
return $this->response->setJSON(['rows' => $out]);
|
||||
}
|
||||
|
||||
public function autotype_keyword_update()
|
||||
{
|
||||
if (! $this->request->isAJAX()) {
|
||||
return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
|
||||
}
|
||||
|
||||
if (! $this->canAccessPrescription()) {
|
||||
return $this->response->setStatusCode(403)->setJSON(['update' => 0, 'error_text' => 'Access denied']);
|
||||
}
|
||||
|
||||
$id = (int) $this->request->getPost('id');
|
||||
$keyword = trim((string) $this->request->getPost('keyword'));
|
||||
$keyword = preg_replace('/\s+/', ' ', $keyword) ?? $keyword;
|
||||
if ($id <= 0 || mb_strlen($keyword) < 2) {
|
||||
return $this->response->setJSON(['update' => 0, 'error_text' => 'Valid keyword is required']);
|
||||
}
|
||||
|
||||
if (! $this->ensureAutotypeKeywordTable()) {
|
||||
return $this->response->setJSON(['update' => 0, 'error_text' => 'Keyword storage unavailable']);
|
||||
}
|
||||
|
||||
$table = $this->db->table('opd_autotype_keyword_master');
|
||||
$current = $table->where('id', $id)->where('is_active', 1)->get(1)->getRowArray();
|
||||
if (empty($current)) {
|
||||
return $this->response->setJSON(['update' => 0, 'error_text' => 'Keyword not found']);
|
||||
}
|
||||
|
||||
$docId = max(0, (int) ($this->getCurrentUserId() ?? 0));
|
||||
$rowDocId = (int) ($current['doc_id'] ?? 0);
|
||||
if ($rowDocId === 0) {
|
||||
if (! $this->canManageMasterKeyword()) {
|
||||
return $this->response->setJSON(['update' => 0, 'error_text' => 'No permission to edit master keyword']);
|
||||
}
|
||||
} elseif ($rowDocId !== $docId) {
|
||||
return $this->response->setJSON(['update' => 0, 'error_text' => 'You can edit only your own keyword']);
|
||||
}
|
||||
|
||||
$section = (string) ($current['section_key'] ?? '');
|
||||
if (! $this->isAutotypeKeywordSectionAllowed($section)) {
|
||||
return $this->response->setJSON(['update' => 0, 'error_text' => 'Unsupported section']);
|
||||
}
|
||||
|
||||
$dup = $table
|
||||
->where('doc_id', $rowDocId)
|
||||
->where('section_key', $section)
|
||||
->where('keyword', $keyword)
|
||||
->where('id <>', $id)
|
||||
->where('is_active', 1)
|
||||
->get(1)
|
||||
->getRowArray();
|
||||
if (! empty($dup)) {
|
||||
return $this->response->setJSON(['update' => 0, 'error_text' => 'Keyword already exists in this scope']);
|
||||
}
|
||||
|
||||
$table->where('id', $id)->update([
|
||||
'keyword' => $keyword,
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
|
||||
return $this->response->setJSON([
|
||||
'update' => 1,
|
||||
'error_text' => 'Keyword updated',
|
||||
'csrfName' => csrf_token(),
|
||||
'csrfHash' => csrf_hash(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function autotype_keyword_remove()
|
||||
{
|
||||
if (! $this->request->isAJAX()) {
|
||||
return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
|
||||
}
|
||||
|
||||
if (! $this->canAccessPrescription()) {
|
||||
return $this->response->setStatusCode(403)->setJSON(['update' => 0, 'error_text' => 'Access denied']);
|
||||
}
|
||||
|
||||
$id = (int) $this->request->getPost('id');
|
||||
if ($id <= 0) {
|
||||
return $this->response->setJSON(['update' => 0, 'error_text' => 'Keyword id is required']);
|
||||
}
|
||||
|
||||
if (! $this->ensureAutotypeKeywordTable()) {
|
||||
return $this->response->setJSON(['update' => 0, 'error_text' => 'Keyword storage unavailable']);
|
||||
}
|
||||
|
||||
$table = $this->db->table('opd_autotype_keyword_master');
|
||||
$current = $table->where('id', $id)->where('is_active', 1)->get(1)->getRowArray();
|
||||
if (empty($current)) {
|
||||
return $this->response->setJSON(['update' => 0, 'error_text' => 'Keyword not found']);
|
||||
}
|
||||
|
||||
$docId = max(0, (int) ($this->getCurrentUserId() ?? 0));
|
||||
$rowDocId = (int) ($current['doc_id'] ?? 0);
|
||||
if ($rowDocId === 0) {
|
||||
if (! $this->canManageMasterKeyword()) {
|
||||
return $this->response->setJSON(['update' => 0, 'error_text' => 'No permission to remove master keyword']);
|
||||
}
|
||||
} elseif ($rowDocId !== $docId) {
|
||||
return $this->response->setJSON(['update' => 0, 'error_text' => 'You can remove only your own keyword']);
|
||||
}
|
||||
|
||||
$table->where('id', $id)->update([
|
||||
'is_active' => 0,
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
|
||||
return $this->response->setJSON([
|
||||
'update' => 1,
|
||||
'error_text' => 'Keyword removed',
|
||||
'csrfName' => csrf_token(),
|
||||
'csrfHash' => csrf_hash(),
|
||||
]);
|
||||
}
|
||||
|
||||
// Legacy-compatible wrappers used by older autocomplete clients.
|
||||
public function get_complaints()
|
||||
{
|
||||
|
|
@ -6322,13 +6511,30 @@ class Opd_prescription extends BaseController
|
|||
is_active TINYINT NOT NULL DEFAULT 1,
|
||||
created_at DATETIME NULL,
|
||||
updated_at DATETIME NULL,
|
||||
UNIQUE KEY uniq_doc_section_keyword (doc_id, section_key, keyword),
|
||||
INDEX idx_section_keyword (section_key, keyword),
|
||||
UNIQUE KEY uniq_doc_section_keyword (doc_id, section_key, keyword(120)),
|
||||
INDEX idx_section_keyword (section_key, keyword(120)),
|
||||
INDEX idx_doc_active (doc_id, is_active)
|
||||
)";
|
||||
$this->db->query($sql);
|
||||
} catch (\Throwable $e) {
|
||||
return false;
|
||||
// Fallback for stricter MySQL/InnoDB index length limits.
|
||||
try {
|
||||
$fallbackSql = "CREATE TABLE IF NOT EXISTS opd_autotype_keyword_master (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
doc_id INT NOT NULL DEFAULT 0,
|
||||
section_key VARCHAR(60) NOT NULL,
|
||||
keyword VARCHAR(180) NOT NULL,
|
||||
is_active TINYINT NOT NULL DEFAULT 1,
|
||||
created_at DATETIME NULL,
|
||||
updated_at DATETIME NULL,
|
||||
UNIQUE KEY uniq_doc_section_keyword (doc_id, section_key, keyword),
|
||||
INDEX idx_section_keyword (section_key, keyword),
|
||||
INDEX idx_doc_active (doc_id, is_active)
|
||||
)";
|
||||
$this->db->query($fallbackSql);
|
||||
} catch (\Throwable $e2) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->db->tableExists('opd_autotype_keyword_master');
|
||||
|
|
@ -6389,6 +6595,16 @@ class Opd_prescription extends BaseController
|
|||
return $out;
|
||||
}
|
||||
|
||||
private function canManageMasterKeyword(): bool
|
||||
{
|
||||
$user = auth()->user();
|
||||
if (! $user || ! method_exists($user, 'can')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $user->can('doctor_work.template_workspace.access') || $user->can('doctor_work.access');
|
||||
}
|
||||
|
||||
private function isTemplateSectionAllowed(string $section): bool
|
||||
{
|
||||
return in_array($section, ['finding_examinations', 'diagnosis', 'provisional_diagnosis', 'prescriber_remarks', 'advice'], true);
|
||||
|
|
|
|||
|
|
@ -467,6 +467,7 @@
|
|||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="badge bg-light text-dark border">Doctor View</span>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" id="btn_toggle_consult_view_settings">Customize Sections</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" id="btn_manage_autotype_keywords">Manage Keywords</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -949,6 +950,91 @@
|
|||
</div>
|
||||
</form>
|
||||
|
||||
<div class="modal fade" id="autotypeKeywordModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Manage Autotype Keywords</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="border rounded p-2 mb-3 bg-light">
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label mb-1">Add In Section</label>
|
||||
<select class="form-select form-select-sm" id="autotype_keyword_add_section">
|
||||
<option value="complaints">Complaints</option>
|
||||
<option value="finding_examinations">Finding/Examinations</option>
|
||||
<option value="provisional_diagnosis">Provisional Diagnosis</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label mb-1">Save Scope</label>
|
||||
<select class="form-select form-select-sm" id="autotype_keyword_add_scope">
|
||||
<option value="doctor">My</option>
|
||||
<option value="master">Master</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<label class="form-label mb-1">Keyword Text</label>
|
||||
<input type="text" class="form-control form-control-sm" id="autotype_keyword_add_text" placeholder="Type full keyword/text...">
|
||||
</div>
|
||||
<div class="col-md-2 d-grid">
|
||||
<button type="button" class="btn btn-primary btn-sm" id="btn_autotype_keyword_add">Add Keyword</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label mb-1">Section</label>
|
||||
<select class="form-select form-select-sm" id="autotype_keyword_filter_section">
|
||||
<option value="">All sections</option>
|
||||
<option value="complaints">Complaints</option>
|
||||
<option value="finding_examinations">Finding/Examinations</option>
|
||||
<option value="provisional_diagnosis">Provisional Diagnosis</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label mb-1">Scope</label>
|
||||
<select class="form-select form-select-sm" id="autotype_keyword_filter_scope">
|
||||
<option value="all">All</option>
|
||||
<option value="doctor">My Keywords</option>
|
||||
<option value="master">Master Keywords</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<label class="form-label mb-1">Search</label>
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" class="form-control" id="autotype_keyword_filter_search" placeholder="Search keyword...">
|
||||
<button type="button" class="btn btn-outline-secondary" id="btn_autotype_keyword_filter_reload">Reload</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="small text-muted mb-2" id="autotype_keyword_manager_note">Showing latest keywords.</div>
|
||||
<div class="table-responsive border rounded">
|
||||
<table class="table table-sm table-striped mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="140">Section</th>
|
||||
<th>Keyword</th>
|
||||
<th width="90">Scope</th>
|
||||
<th width="110">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="autotype_keyword_manager_rows">
|
||||
<tr><td colspan="4" class="text-muted px-2 py-2">No keyword found.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="rxTemplateModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-md">
|
||||
<div class="modal-content">
|
||||
|
|
@ -1200,6 +1286,12 @@
|
|||
var autotypeSuggestCacheMaxEntries = 240;
|
||||
var autotypeSuggestCacheLocalKey = 'opd_autotype_suggest_cache_v1';
|
||||
var autotypeSuggestCacheLocalReady = false;
|
||||
var autotypeKeywordModalLoading = false;
|
||||
var autotypeKeywordSectionLabels = {
|
||||
complaints: 'Complaints',
|
||||
finding_examinations: 'Finding/Examinations',
|
||||
provisional_diagnosis: 'Provisional Diagnosis'
|
||||
};
|
||||
var medInputTimer = null;
|
||||
var crgcMedInputTimer = null;
|
||||
var medicineDoseMasterCache = { dose: [], when: [], freq: [], where: [] };
|
||||
|
|
@ -1586,6 +1678,72 @@
|
|||
return (val || '').toString().split(/,\s*/);
|
||||
}
|
||||
|
||||
function getTextareaCaretCoordinates(el, caretPos) {
|
||||
var doc = document;
|
||||
var div = doc.createElement('div');
|
||||
var style = window.getComputedStyle(el);
|
||||
var propList = [
|
||||
'boxSizing', 'width', 'height', 'overflowX', 'overflowY',
|
||||
'borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth',
|
||||
'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft',
|
||||
'fontStyle', 'fontVariant', 'fontWeight', 'fontStretch', 'fontSize',
|
||||
'fontSizeAdjust', 'lineHeight', 'fontFamily', 'textAlign', 'textTransform',
|
||||
'textIndent', 'textDecoration', 'letterSpacing', 'wordSpacing'
|
||||
];
|
||||
|
||||
div.style.position = 'absolute';
|
||||
div.style.visibility = 'hidden';
|
||||
div.style.whiteSpace = 'pre-wrap';
|
||||
div.style.wordWrap = 'break-word';
|
||||
div.style.left = '-9999px';
|
||||
propList.forEach(function(prop) {
|
||||
div.style[prop] = style[prop];
|
||||
});
|
||||
|
||||
div.textContent = el.value.substring(0, caretPos);
|
||||
var span = doc.createElement('span');
|
||||
span.textContent = el.value.substring(caretPos) || '.';
|
||||
div.appendChild(span);
|
||||
doc.body.appendChild(div);
|
||||
|
||||
var top = span.offsetTop - el.scrollTop;
|
||||
var left = span.offsetLeft - el.scrollLeft;
|
||||
doc.body.removeChild(div);
|
||||
|
||||
return { top: top, left: left };
|
||||
}
|
||||
|
||||
function positionAutocompleteNearCaret($field) {
|
||||
var ac = $field.autocomplete('instance');
|
||||
if (!ac || !ac.menu || !ac.menu.element || !$field.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var el = $field.get(0);
|
||||
if (!el || el.tagName !== 'TEXTAREA') {
|
||||
return;
|
||||
}
|
||||
|
||||
var offset = $field.offset();
|
||||
if (!offset) {
|
||||
return;
|
||||
}
|
||||
|
||||
var caretPos = Number(el.selectionStart || 0);
|
||||
var caret = getTextareaCaretCoordinates(el, caretPos);
|
||||
var lineHeight = parseFloat(window.getComputedStyle(el).lineHeight || '20');
|
||||
var menuLeft = Math.max(8, offset.left + caret.left);
|
||||
var menuTop = Math.max(8, offset.top + caret.top + lineHeight + 6);
|
||||
|
||||
ac.menu.element.css({
|
||||
position: 'absolute',
|
||||
left: menuLeft + 'px',
|
||||
top: menuTop + 'px',
|
||||
minWidth: Math.max(260, Math.round($field.innerWidth() * 0.65)) + 'px',
|
||||
zIndex: 2000
|
||||
});
|
||||
}
|
||||
|
||||
function extractLastCommaTerm(term) {
|
||||
return splitCommaTerms(term).pop() || '';
|
||||
}
|
||||
|
|
@ -1668,11 +1826,14 @@
|
|||
return token;
|
||||
}
|
||||
|
||||
function saveAutotypeKeyword(section, keyword, done) {
|
||||
var scope = 'doctor';
|
||||
var useMaster = window.confirm('Save keyword to Master list for all doctors?\nChoose Cancel to save only for current doctor.');
|
||||
if (useMaster) {
|
||||
scope = 'master';
|
||||
function saveAutotypeKeyword(section, keyword, done, scopeOverride) {
|
||||
var scope = (scopeOverride || '').toString().trim().toLowerCase();
|
||||
if (scope !== 'doctor' && scope !== 'master') {
|
||||
scope = 'doctor';
|
||||
var useMaster = window.confirm('Save keyword to Master list for all doctors?\nChoose Cancel to save only for current doctor.');
|
||||
if (useMaster) {
|
||||
scope = 'master';
|
||||
}
|
||||
}
|
||||
|
||||
apiPost('<?= base_url('Opd_prescription/autotype_keyword_save') ?>', {
|
||||
|
|
@ -1690,6 +1851,137 @@
|
|||
});
|
||||
}
|
||||
|
||||
function autotypeKeywordApiList(params, done) {
|
||||
$.getJSON('<?= base_url('Opd_prescription/autotype_keyword_list') ?>', params || {}, function(data) {
|
||||
if (typeof done === 'function') {
|
||||
done(data || {});
|
||||
}
|
||||
}).fail(function() {
|
||||
if (typeof done === 'function') {
|
||||
done({ rows: [], error_text: 'Unable to fetch keyword list.' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function autotypeKeywordApiUpdate(id, keyword, done) {
|
||||
apiPost('<?= base_url('Opd_prescription/autotype_keyword_update') ?>', {
|
||||
id: id,
|
||||
keyword: keyword
|
||||
}, function(data) {
|
||||
if (typeof done === 'function') {
|
||||
done(data || {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function autotypeKeywordApiRemove(id, done) {
|
||||
apiPost('<?= base_url('Opd_prescription/autotype_keyword_remove') ?>', {
|
||||
id: id
|
||||
}, function(data) {
|
||||
if (typeof done === 'function') {
|
||||
done(data || {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderAutotypeKeywordManagerRows(rows) {
|
||||
var $tbody = $('#autotype_keyword_manager_rows');
|
||||
$tbody.empty();
|
||||
|
||||
var list = Array.isArray(rows) ? rows : [];
|
||||
if (!list.length) {
|
||||
$tbody.append('<tr><td colspan="4" class="text-muted px-2 py-2">No keyword found.</td></tr>');
|
||||
return;
|
||||
}
|
||||
|
||||
list.forEach(function(row) {
|
||||
var id = Number(row.id || 0);
|
||||
var keyword = (row.keyword || '').toString();
|
||||
var section = (row.section_key || '').toString();
|
||||
var scopeLabel = (row.scope_label || row.scope || '').toString();
|
||||
var canEdit = Number(row.can_edit || 0) === 1;
|
||||
var canDelete = Number(row.can_delete || 0) === 1;
|
||||
|
||||
var $tr = $('<tr></tr>');
|
||||
$('<td></td>').text(autotypeKeywordSectionLabels[section] || section || '-').appendTo($tr);
|
||||
$('<td></td>').text(keyword).appendTo($tr);
|
||||
$('<td></td>').text(scopeLabel).appendTo($tr);
|
||||
|
||||
var $action = $('<td></td>');
|
||||
var $editBtn = $('<button type="button" class="btn btn-outline-primary btn-sm me-1">Edit</button>');
|
||||
var $delBtn = $('<button type="button" class="btn btn-outline-danger btn-sm">Delete</button>');
|
||||
$editBtn.attr('data-id', id).attr('data-keyword', keyword).addClass('btn-autotype-keyword-edit');
|
||||
$delBtn.attr('data-id', id).attr('data-keyword', keyword).addClass('btn-autotype-keyword-delete');
|
||||
|
||||
if (!canEdit) {
|
||||
$editBtn.prop('disabled', true);
|
||||
}
|
||||
if (!canDelete) {
|
||||
$delBtn.prop('disabled', true);
|
||||
}
|
||||
|
||||
$action.append($editBtn).append($delBtn).appendTo($tr);
|
||||
$tbody.append($tr);
|
||||
});
|
||||
}
|
||||
|
||||
function getAutotypeKeywordFilters() {
|
||||
return {
|
||||
section: ($('#autotype_keyword_filter_section').val() || '').toString(),
|
||||
scope: ($('#autotype_keyword_filter_scope').val() || 'all').toString(),
|
||||
q: ($('#autotype_keyword_filter_search').val() || '').toString().trim()
|
||||
};
|
||||
}
|
||||
|
||||
function submitModalAutotypeKeyword() {
|
||||
var section = ($('#autotype_keyword_add_section').val() || '').toString();
|
||||
var scope = ($('#autotype_keyword_add_scope').val() || 'doctor').toString().toLowerCase();
|
||||
var keyword = ($('#autotype_keyword_add_text').val() || '').toString().trim();
|
||||
|
||||
if (!section) {
|
||||
$('.jsError').removeClass('text-success text-muted').addClass('text-danger').text('Please select section for keyword.');
|
||||
return;
|
||||
}
|
||||
if (!keyword || keyword.length < 2) {
|
||||
$('.jsError').removeClass('text-success text-muted').addClass('text-danger').text('Please type at least 2 characters in keyword text.');
|
||||
return;
|
||||
}
|
||||
|
||||
saveAutotypeKeyword(section, keyword, function(data) {
|
||||
if (Number(data.update || 0) === 1) {
|
||||
clearAutotypeSuggestionCaches();
|
||||
$('#autotype_keyword_add_text').val('');
|
||||
$('.jsError').removeClass('text-danger text-muted').addClass('text-success').text((data.error_text || 'Keyword saved') + ': ' + keyword);
|
||||
loadAutotypeKeywordManagerRows();
|
||||
} else {
|
||||
$('.jsError').removeClass('text-success text-muted').addClass('text-danger').text(data.error_text || 'Unable to save keyword.');
|
||||
}
|
||||
}, scope);
|
||||
}
|
||||
|
||||
function loadAutotypeKeywordManagerRows() {
|
||||
if (autotypeKeywordModalLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
autotypeKeywordModalLoading = true;
|
||||
$('#autotype_keyword_manager_note').text('Loading keywords...');
|
||||
autotypeKeywordApiList(getAutotypeKeywordFilters(), function(data) {
|
||||
autotypeKeywordModalLoading = false;
|
||||
var rows = (data && data.rows) ? data.rows : [];
|
||||
renderAutotypeKeywordManagerRows(rows);
|
||||
$('#autotype_keyword_manager_note').text('Showing ' + rows.length + ' keyword(s).');
|
||||
if (data && data.error_text) {
|
||||
$('.jsError').removeClass('text-success text-muted').addClass('text-danger').text(data.error_text);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function clearAutotypeSuggestionCaches() {
|
||||
autotypeSuggestCache = {};
|
||||
persistAutotypeSuggestCache();
|
||||
}
|
||||
|
||||
function bindLegacyCommaAutocomplete(selector, endpointUrl, extractValue) {
|
||||
var $field = $(selector);
|
||||
if (!$field.length || !($.ui && $.ui.autocomplete)) {
|
||||
|
|
@ -1744,6 +2036,9 @@
|
|||
focus: function() {
|
||||
return false;
|
||||
},
|
||||
open: function() {
|
||||
positionAutocompleteNearCaret($(this));
|
||||
},
|
||||
select: function(event, ui) {
|
||||
var terms = splitCommaTerms(this.value);
|
||||
terms.pop();
|
||||
|
|
@ -1756,6 +2051,14 @@
|
|||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
$field.on('keyup click focus', function() {
|
||||
var $self = $(this);
|
||||
var ac = $self.autocomplete('instance');
|
||||
if (ac && ac.menu && ac.menu.element && ac.menu.element.is(':visible')) {
|
||||
positionAutocompleteNearCaret($self);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getCsrfPair() {
|
||||
|
|
@ -5963,12 +6266,103 @@
|
|||
saveAutotypeKeyword(section, keyword, function(data) {
|
||||
if (Number(data.update || 0) === 1) {
|
||||
$('.jsError').removeClass('text-danger text-muted').addClass('text-success').text((data.error_text || 'Keyword saved') + ': ' + keyword);
|
||||
loadAutotypeKeywordManagerRows();
|
||||
} else {
|
||||
$('.jsError').removeClass('text-success text-muted').addClass('text-danger').text(data.error_text || 'Unable to save keyword.');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('#btn_manage_autotype_keywords').on('click', function() {
|
||||
var firstSection = ($('#autotype_keyword_filter_section').val() || 'complaints').toString();
|
||||
if (!firstSection) {
|
||||
firstSection = 'complaints';
|
||||
}
|
||||
$('#autotype_keyword_add_section').val(firstSection);
|
||||
loadAutotypeKeywordManagerRows();
|
||||
if (window.bootstrap && window.bootstrap.Modal) {
|
||||
window.bootstrap.Modal.getOrCreateInstance(document.getElementById('autotypeKeywordModal')).show();
|
||||
} else {
|
||||
$('#autotypeKeywordModal').show();
|
||||
}
|
||||
});
|
||||
|
||||
$('#btn_autotype_keyword_add').on('click', function() {
|
||||
submitModalAutotypeKeyword();
|
||||
});
|
||||
|
||||
$('#autotype_keyword_add_text').on('keydown', function(e) {
|
||||
if ((e.key || '').toLowerCase() === 'enter') {
|
||||
e.preventDefault();
|
||||
submitModalAutotypeKeyword();
|
||||
}
|
||||
});
|
||||
|
||||
$('#btn_autotype_keyword_filter_reload').on('click', function() {
|
||||
loadAutotypeKeywordManagerRows();
|
||||
});
|
||||
|
||||
$('#autotype_keyword_filter_section, #autotype_keyword_filter_scope').on('change', function() {
|
||||
loadAutotypeKeywordManagerRows();
|
||||
});
|
||||
|
||||
$('#autotype_keyword_filter_search').on('keydown', function(e) {
|
||||
if ((e.key || '').toLowerCase() === 'enter') {
|
||||
e.preventDefault();
|
||||
loadAutotypeKeywordManagerRows();
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on('click', '.btn-autotype-keyword-edit', function() {
|
||||
var id = Number($(this).data('id') || 0);
|
||||
var oldKeyword = ($(this).data('keyword') || '').toString();
|
||||
if (id <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var next = window.prompt('Edit keyword', oldKeyword);
|
||||
if (next === null) {
|
||||
return;
|
||||
}
|
||||
next = next.toString().trim();
|
||||
if (next.length < 2) {
|
||||
$('.jsError').removeClass('text-success text-muted').addClass('text-danger').text('Keyword must be at least 2 characters.');
|
||||
return;
|
||||
}
|
||||
|
||||
autotypeKeywordApiUpdate(id, next, function(data) {
|
||||
if (Number(data.update || 0) === 1) {
|
||||
clearAutotypeSuggestionCaches();
|
||||
$('.jsError').removeClass('text-danger text-muted').addClass('text-success').text(data.error_text || 'Keyword updated.');
|
||||
loadAutotypeKeywordManagerRows();
|
||||
} else {
|
||||
$('.jsError').removeClass('text-success text-muted').addClass('text-danger').text(data.error_text || 'Unable to update keyword.');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$(document).on('click', '.btn-autotype-keyword-delete', function() {
|
||||
var id = Number($(this).data('id') || 0);
|
||||
var keyword = ($(this).data('keyword') || '').toString();
|
||||
if (id <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!window.confirm('Delete keyword: ' + keyword + ' ?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
autotypeKeywordApiRemove(id, function(data) {
|
||||
if (Number(data.update || 0) === 1) {
|
||||
clearAutotypeSuggestionCaches();
|
||||
$('.jsError').removeClass('text-danger text-muted').addClass('text-success').text(data.error_text || 'Keyword removed.');
|
||||
loadAutotypeKeywordManagerRows();
|
||||
} else {
|
||||
$('.jsError').removeClass('text-success text-muted').addClass('text-danger').text(data.error_text || 'Unable to remove keyword.');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
initConsultSectionDesigner();
|
||||
|
||||
bindLegacyCommaAutocomplete('#complaints', '<?= base_url('Opd_prescription/complaints_search') ?>', function(row) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user