feat: Healthplix-style complaint table — inline row editing with Frequency/Severity/Duration/Date columns, remove Extract Codes button
This commit is contained in:
parent
e275a20946
commit
9fe9c971f1
|
|
@ -851,47 +851,40 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3" id="rx_sec_complaints">
|
<div class="mb-3" id="rx_sec_complaints">
|
||||||
<h6 class="mb-2">Chief Complaints</h6>
|
<div class="d-flex align-items-center justify-content-between mb-2">
|
||||||
|
<h6 class="mb-0">Chief Complaints</h6>
|
||||||
<!-- Search -->
|
<button type="button" class="btn btn-sm btn-outline-secondary btn-save-autotype-keyword"
|
||||||
<div class="mb-2 position-relative">
|
data-section="complaints" data-target="complaints"
|
||||||
<div class="input-group input-group-sm">
|
style="font-size:.75rem" title="Save as custom keyword">+keyword</button>
|
||||||
<input type="text" class="form-control" id="complaint_lookup"
|
|
||||||
autocomplete="off"
|
|
||||||
placeholder="Type symptom or free text: headache 3 days moderate, fever 1 day...">
|
|
||||||
<button type="button" class="btn btn-outline-primary" id="btn_extract_complaint_codes"
|
|
||||||
title="Extract SNOMED codes from typed text with duration & severity">Extract Codes</button>
|
|
||||||
<button type="button" class="btn btn-outline-secondary btn-save-autotype-keyword"
|
|
||||||
data-section="complaints" data-target="complaints" title="Save as custom keyword">+keyword</button>
|
|
||||||
</div>
|
|
||||||
<div id="complaint_dropdown" class="border rounded bg-white shadow-sm"
|
|
||||||
style="display:none;position:absolute;left:0;right:0;top:100%;z-index:1060;max-height:260px;overflow-y:auto;"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- SNOMED Extract chips panel -->
|
<!-- Healthplix-style inline complaint table -->
|
||||||
<div id="complaint_extract_panel" class="mb-2 p-2 border rounded" style="display:none;background:#f8faff;">
|
<table class="table table-sm table-bordered align-middle mb-1" id="complaint_table" style="font-size:.82rem">
|
||||||
<div class="d-flex align-items-center gap-2 mb-2">
|
|
||||||
<span class="small fw-semibold text-primary">💡 Code Suggestions</span>
|
|
||||||
<button type="button" class="btn btn-sm btn-success py-0 px-2" id="btn_confirm_all_chips" style="font-size:.75rem">✓ Confirm All</button>
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary py-0 px-2" id="btn_dismiss_extract" style="font-size:.75rem">Dismiss</button>
|
|
||||||
</div>
|
|
||||||
<div id="complaint_chips_wrap" class="d-flex flex-wrap gap-2"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Complaint rows table -->
|
|
||||||
<table class="table table-sm table-bordered align-middle mb-1" id="complaint_table" style="display:none;font-size:.82rem">
|
|
||||||
<thead class="table-light" style="font-size:.75rem">
|
<thead class="table-light" style="font-size:.75rem">
|
||||||
<tr>
|
<tr>
|
||||||
|
<th style="width:28px">#</th>
|
||||||
<th>Complaint</th>
|
<th>Complaint</th>
|
||||||
<th style="width:100px">Code</th>
|
<th style="width:110px">Frequency</th>
|
||||||
<th style="width:120px">Duration</th>
|
|
||||||
<th style="width:100px">Severity</th>
|
<th style="width:100px">Severity</th>
|
||||||
|
<th style="width:120px">Duration</th>
|
||||||
|
<th style="width:105px">Date</th>
|
||||||
<th style="width:24px"></th>
|
<th style="width:24px"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="complaint_tbody"></tbody>
|
<tbody id="complaint_tbody"></tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td colspan="5" class="p-1 position-relative">
|
||||||
|
<input type="text" class="form-control form-control-sm" id="complaint_lookup"
|
||||||
|
autocomplete="off" placeholder="Type complaint to add…">
|
||||||
|
<div id="complaint_dropdown" class="border rounded bg-white shadow-sm"
|
||||||
|
style="display:none;position:absolute;left:0;right:0;top:100%;z-index:1060;max-height:260px;overflow-y:auto;"></div>
|
||||||
|
</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
</table>
|
</table>
|
||||||
<div id="complaint_empty_hint" class="text-muted small mb-1">No complaint selected yet</div>
|
|
||||||
|
|
||||||
<!-- Fixed dropdowns for table cell inputs -->
|
<!-- Fixed dropdowns for table cell inputs -->
|
||||||
<div id="complaint_dur_dd" style="display:none;position:fixed;z-index:1090;min-width:160px;background:#fff;border:1px solid #dee2e6;border-radius:.375rem;box-shadow:0 4px 12px rgba(0,0,0,.12);max-height:180px;overflow-y:auto;"></div>
|
<div id="complaint_dur_dd" style="display:none;position:fixed;z-index:1090;min-width:160px;background:#fff;border:1px solid #dee2e6;border-radius:.375rem;box-shadow:0 4px 12px rgba(0,0,0,.12);max-height:180px;overflow-y:auto;"></div>
|
||||||
|
|
@ -4687,8 +4680,10 @@
|
||||||
concept_id: item.concept_id || '',
|
concept_id: item.concept_id || '',
|
||||||
source: item.source || '',
|
source: item.source || '',
|
||||||
hierarchy: item.hierarchy || '',
|
hierarchy: item.hierarchy || '',
|
||||||
|
frequency: item.frequency || '',
|
||||||
|
severity: item.severity || '',
|
||||||
duration: item.duration || '',
|
duration: item.duration || '',
|
||||||
severity: item.severity || ''
|
date: item.date || ''
|
||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
$('#complaint_snomed_json').val(json);
|
$('#complaint_snomed_json').val(json);
|
||||||
|
|
@ -4702,53 +4697,53 @@
|
||||||
var $tbody = $('#complaint_tbody');
|
var $tbody = $('#complaint_tbody');
|
||||||
$tbody.empty();
|
$tbody.empty();
|
||||||
|
|
||||||
if (!selectedComplaintItems.length) {
|
|
||||||
$('#complaint_table').hide();
|
|
||||||
$('#complaint_empty_hint').show();
|
|
||||||
syncComplaintSnomedJson();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$('#complaint_empty_hint').hide();
|
|
||||||
$('#complaint_table').show();
|
|
||||||
|
|
||||||
selectedComplaintItems.forEach(function(item, idx) {
|
selectedComplaintItems.forEach(function(item, idx) {
|
||||||
var isSnomed = !!(item.concept_id);
|
var isSnomed = !!(item.concept_id);
|
||||||
var $tr = $('<tr>').attr('data-idx', idx);
|
var $tr = $('<tr>').attr('data-idx', idx);
|
||||||
|
|
||||||
// Complaint name — SNOMED = blue text, local = muted gray
|
// # column
|
||||||
var $nameTd = $('<td style="max-width:200px">');
|
$tr.append($('<td class="text-center text-muted" style="font-size:.75rem">').text(idx + 1));
|
||||||
var nameColor = isSnomed ? 'color:#0d6efd;font-weight:600' : 'color:#6c757d;font-weight:600';
|
|
||||||
$nameTd.append($('<div class="text-truncate" style="max-width:190px;' + nameColor + '">').text(item.term));
|
// Complaint name — SNOMED = blue, local = default
|
||||||
|
var $nameTd = $('<td style="max-width:220px">');
|
||||||
|
var nameStyle = isSnomed ? 'color:#0d6efd;font-weight:600;max-width:210px' : 'max-width:210px';
|
||||||
|
var $nameDiv = $('<div class="text-truncate" style="' + nameStyle + ';">').text(item.term);
|
||||||
|
if (item.concept_id) { $nameDiv.attr('title', 'SNOMED: ' + item.concept_id); }
|
||||||
|
$nameTd.append($nameDiv);
|
||||||
$tr.append($nameTd);
|
$tr.append($nameTd);
|
||||||
|
|
||||||
// SNOMED code
|
// Frequency input
|
||||||
var $codeComplaintTd = $('<td class="text-center" style="width:100px">');
|
|
||||||
if (item.concept_id) {
|
|
||||||
$codeComplaintTd.append(
|
|
||||||
$('<span class="badge text-bg-light border" style="font-size:.7rem;font-family:monospace;letter-spacing:.02em">').text(item.concept_id)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
$codeComplaintTd.append($('<span class="text-muted" style="font-size:.75rem">').text('\u2014'));
|
|
||||||
}
|
|
||||||
$tr.append($codeComplaintTd);
|
|
||||||
|
|
||||||
// Duration input
|
|
||||||
$tr.append(
|
$tr.append(
|
||||||
$('<td class="p-1">').append(
|
$('<td class="p-1">').append(
|
||||||
$('<input type="text" class="form-control form-control-sm complaint-dur-input" autocomplete="off" placeholder="2 days...">')
|
$('<input type="text" class="form-control form-control-sm complaint-freq-input" autocomplete="off" placeholder="daily…">')
|
||||||
.val(item.duration || '').attr('data-idx', idx)
|
.val(item.frequency || '').attr('data-idx', idx)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Severity input
|
// Severity input
|
||||||
$tr.append(
|
$tr.append(
|
||||||
$('<td class="p-1">').append(
|
$('<td class="p-1">').append(
|
||||||
$('<input type="text" class="form-control form-control-sm complaint-sev-input" autocomplete="off" placeholder="mild...">')
|
$('<input type="text" class="form-control form-control-sm complaint-sev-input" autocomplete="off" placeholder="mild…">')
|
||||||
.val(item.severity || '').attr('data-idx', idx)
|
.val(item.severity || '').attr('data-idx', idx)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Duration input
|
||||||
|
$tr.append(
|
||||||
|
$('<td class="p-1">').append(
|
||||||
|
$('<input type="text" class="form-control form-control-sm complaint-dur-input" autocomplete="off" placeholder="2 days…">')
|
||||||
|
.val(item.duration || '').attr('data-idx', idx)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Date input
|
||||||
|
$tr.append(
|
||||||
|
$('<td class="p-1">').append(
|
||||||
|
$('<input type="date" class="form-control form-control-sm complaint-date-input">')
|
||||||
|
.val(item.date || '').attr('data-idx', idx)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
// Remove button
|
// Remove button
|
||||||
$tr.append(
|
$tr.append(
|
||||||
$('<td class="p-1 text-center">').append(
|
$('<td class="p-1 text-center">').append(
|
||||||
|
|
@ -4783,123 +4778,24 @@
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedComplaintItems.push({ term: term, concept_id: conceptId, source: source, hierarchy: hierarchy, duration: '', severity: '' });
|
selectedComplaintItems.push({ term: term, concept_id: conceptId, source: source, hierarchy: hierarchy,
|
||||||
|
frequency: '', severity: '', duration: '', date: '' });
|
||||||
var newIdx = selectedComplaintItems.length - 1;
|
var newIdx = selectedComplaintItems.length - 1;
|
||||||
renderComplaintTable();
|
renderComplaintTable();
|
||||||
appendComplaintToTextarea(term);
|
appendComplaintToTextarea(term);
|
||||||
refreshCounters();
|
refreshCounters();
|
||||||
markDirty('Complaint added');
|
markDirty('Complaint added');
|
||||||
// Focus the new row's Duration input
|
// Focus the new row's Frequency input
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
$('#complaint_tbody').find('.complaint-dur-input[data-idx="' + newIdx + '"]').trigger('focus');
|
$('#complaint_tbody').find('.complaint-freq-input[data-idx="' + newIdx + '"]').trigger('focus');
|
||||||
}, 60);
|
}, 60);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Extract Codes from free-text (SNOMED chips review) ─────────────────
|
|
||||||
$('#btn_extract_complaint_codes').on('click', function () {
|
|
||||||
var text = $.trim($('#complaint_lookup').val());
|
|
||||||
if (!text) { $('#complaint_lookup').trigger('focus'); return; }
|
|
||||||
|
|
||||||
var $btn = $(this);
|
|
||||||
var $panel = $('#complaint_extract_panel');
|
|
||||||
var $wrap = $('#complaint_chips_wrap');
|
|
||||||
|
|
||||||
$btn.prop('disabled', true).text('Extracting…');
|
|
||||||
$panel.show();
|
|
||||||
$wrap.html('<span class="text-muted small">Searching SNOMED…</span>');
|
|
||||||
|
|
||||||
$.get('<?= base_url('Opd_prescription/complaints_extract') ?>', { q: text })
|
|
||||||
.done(function (data) {
|
|
||||||
$btn.prop('disabled', false).text('Extract Codes');
|
|
||||||
var rows = (data && data.rows) ? data.rows : [];
|
|
||||||
|
|
||||||
if (!rows.length) {
|
|
||||||
$wrap.html('<span class="text-muted small">No SNOMED match. Added as free text.</span>');
|
|
||||||
addComplaintItem({ name: text, concept_id: '', source: 'local' });
|
|
||||||
setTimeout(function () { $panel.hide(); $wrap.empty(); }, 1800);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$wrap.empty();
|
|
||||||
rows.forEach(function (r) {
|
|
||||||
var pct = Math.round((parseFloat(r.confidence) || 0) * 100);
|
|
||||||
var meta = [r.duration, r.severity].filter(Boolean).join(' · ');
|
|
||||||
|
|
||||||
var $chip = $('<span class="snomed-chip"></span>');
|
|
||||||
$chip.append($('<span class="chip-term"></span>').text(r.term || r.snomed_term || ''));
|
|
||||||
if (r.concept_id) {
|
|
||||||
$chip.append($('<span class="chip-code"></span>').text(' ' + r.concept_id));
|
|
||||||
}
|
|
||||||
if (meta) {
|
|
||||||
$chip.append($('<span class="chip-meta"></span>').text(' · ' + meta));
|
|
||||||
}
|
|
||||||
if (pct > 0) {
|
|
||||||
$chip.append($('<span class="chip-conf"></span>').text(pct + '%'));
|
|
||||||
}
|
|
||||||
|
|
||||||
var $bConfirm = $('<button type="button" class="chip-btn chip-btn-confirm" title="Add complaint">✓</button>');
|
|
||||||
var $bReject = $('<button type="button" class="chip-btn chip-btn-reject" title="Dismiss">✕</button>');
|
|
||||||
|
|
||||||
$bConfirm.on('click', function () {
|
|
||||||
var added = addComplaintItem({
|
|
||||||
name: r.term || r.snomed_term || text,
|
|
||||||
concept_id: r.concept_id || '',
|
|
||||||
source: r.concept_id ? 'snomed' : 'local',
|
|
||||||
hierarchy: r.hierarchy || r.semantic_tag || ''
|
|
||||||
});
|
|
||||||
// Pre-fill duration & severity on the new table row
|
|
||||||
var newIdx = selectedComplaintItems.length - 1;
|
|
||||||
if (newIdx >= 0) {
|
|
||||||
if (r.duration) {
|
|
||||||
selectedComplaintItems[newIdx].duration = r.duration;
|
|
||||||
$('#complaint_tbody .complaint-dur-input[data-idx="' + newIdx + '"]').val(r.duration);
|
|
||||||
}
|
|
||||||
if (r.severity) {
|
|
||||||
selectedComplaintItems[newIdx].severity = r.severity;
|
|
||||||
$('#complaint_tbody .complaint-sev-input[data-idx="' + newIdx + '"]').val(r.severity);
|
|
||||||
}
|
|
||||||
syncComplaintSnomedJson();
|
|
||||||
}
|
|
||||||
$chip.addClass('chip-confirmed');
|
|
||||||
$bConfirm.prop('disabled', true);
|
|
||||||
$bReject.hide();
|
|
||||||
// Clear the lookup input after first confirmation
|
|
||||||
$('#complaint_lookup').val('');
|
|
||||||
});
|
|
||||||
|
|
||||||
$bReject.on('click', function () {
|
|
||||||
$chip.addClass('chip-rejected');
|
|
||||||
if (!$wrap.find('.snomed-chip:not(.chip-rejected)').length) {
|
|
||||||
$panel.hide(); $wrap.empty();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$chip.append($bConfirm).append($bReject);
|
|
||||||
$wrap.append($chip);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.fail(function () {
|
|
||||||
$btn.prop('disabled', false).text('Extract Codes');
|
|
||||||
$wrap.html('<span class="text-danger small">Search failed.</span>');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#btn_confirm_all_chips').on('click', function () {
|
|
||||||
$('#complaint_chips_wrap .snomed-chip:not(.chip-rejected):not(.chip-confirmed) .chip-btn-confirm').each(function () {
|
|
||||||
$(this).trigger('click');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#btn_dismiss_extract').on('click', function () {
|
|
||||||
$('#complaint_extract_panel').hide();
|
|
||||||
$('#complaint_chips_wrap').empty();
|
|
||||||
});
|
|
||||||
|
|
||||||
// ─── Per-complaint inline editor — stubs (table replaced the panel) ─────
|
// ─── Per-complaint inline editor — stubs (table replaced the panel) ─────
|
||||||
function openComplaintInlineEditor(idx) {
|
function openComplaintInlineEditor(idx) {
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
$('#complaint_tbody').find('.complaint-dur-input[data-idx="' + idx + '"]').trigger('focus');
|
$('#complaint_tbody').find('.complaint-freq-input[data-idx="' + idx + '"]').trigger('focus');
|
||||||
}, 60);
|
}, 60);
|
||||||
}
|
}
|
||||||
function closeComplaintInlineEditor() { /* no-op: table is always visible */ }
|
function closeComplaintInlineEditor() { /* no-op: table is always visible */ }
|
||||||
|
|
@ -4940,6 +4836,57 @@
|
||||||
.on('click', function() { onSelect(text); });
|
.on('click', function() { onSelect(text); });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Frequency: event delegation on table rows ────────────────────
|
||||||
|
var _FREQUENCY_OPTIONS = ['daily', 'twice daily', 'weekly', 'intermittent', 'continuous', 'occasional'];
|
||||||
|
|
||||||
|
$(document).on('input focus', '.complaint-freq-input', function() {
|
||||||
|
var $inp = $(this);
|
||||||
|
var q = ($inp.val() || '').trim().toLowerCase();
|
||||||
|
var sugs = q ? _FREQUENCY_OPTIONS.filter(function(s) { return s.indexOf(q) !== -1; }) : _FREQUENCY_OPTIONS;
|
||||||
|
var idx = parseInt($inp.attr('data-idx'), 10);
|
||||||
|
if (!sugs.length) { return; }
|
||||||
|
// reuse complaint_sev_dd div as a floating list (same fixed-position technique)
|
||||||
|
var $dd = $('<div style="position:fixed;z-index:1095;min-width:160px;background:#fff;border:1px solid #dee2e6;border-radius:.375rem;box-shadow:0 4px 12px rgba(0,0,0,.12);max-height:180px;overflow-y:auto;" id="_freq_dd"></div>');
|
||||||
|
$('#_freq_dd').remove();
|
||||||
|
sugs.forEach(function(s) {
|
||||||
|
$dd.append(buildSmartDdItem(s, function(val) {
|
||||||
|
$inp.val(val);
|
||||||
|
if (idx >= 0 && idx < selectedComplaintItems.length) {
|
||||||
|
selectedComplaintItems[idx].frequency = val;
|
||||||
|
syncComplaintSnomedJson();
|
||||||
|
}
|
||||||
|
$('#_freq_dd').remove();
|
||||||
|
$inp.closest('tr').find('.complaint-sev-input').trigger('focus');
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
var r = $inp[0].getBoundingClientRect();
|
||||||
|
$dd.css({ top: (r.bottom + 2) + 'px', left: r.left + 'px', width: Math.max(r.width, 160) + 'px' });
|
||||||
|
$('body').append($dd);
|
||||||
|
});
|
||||||
|
$(document).on('blur', '.complaint-freq-input', function() {
|
||||||
|
var idx = parseInt($(this).attr('data-idx'), 10);
|
||||||
|
if (idx >= 0 && idx < selectedComplaintItems.length) {
|
||||||
|
selectedComplaintItems[idx].frequency = ($(this).val() || '').trim();
|
||||||
|
syncComplaintSnomedJson();
|
||||||
|
}
|
||||||
|
setTimeout(function() { $('#_freq_dd').remove(); }, 150);
|
||||||
|
});
|
||||||
|
$(document).on('click', function(e) {
|
||||||
|
if (!$(e.target).closest('.complaint-freq-input, #_freq_dd').length) {
|
||||||
|
$('#_freq_dd').remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Date input save
|
||||||
|
$(document).on('change', '.complaint-date-input', function() {
|
||||||
|
var idx = parseInt($(this).attr('data-idx'), 10);
|
||||||
|
if (idx >= 0 && idx < selectedComplaintItems.length) {
|
||||||
|
selectedComplaintItems[idx].date = ($(this).val() || '').trim();
|
||||||
|
syncComplaintSnomedJson();
|
||||||
|
markDirty('Complaint date updated');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ─── Duration: event delegation on table rows ────────────────────────────
|
// ─── Duration: event delegation on table rows ────────────────────────────
|
||||||
$(document).on('input focus', '.complaint-dur-input', function() {
|
$(document).on('input focus', '.complaint-dur-input', function() {
|
||||||
_durDdIdx = -1;
|
_durDdIdx = -1;
|
||||||
|
|
@ -5258,8 +5205,10 @@
|
||||||
concept_id: (item.concept_id || '').toString(),
|
concept_id: (item.concept_id || '').toString(),
|
||||||
source: (item.source || '').toString(),
|
source: (item.source || '').toString(),
|
||||||
hierarchy: (item.hierarchy || '').toString(),
|
hierarchy: (item.hierarchy || '').toString(),
|
||||||
|
frequency: (item.frequency || '').toString(),
|
||||||
|
severity: (item.severity || '').toString(),
|
||||||
duration: (item.duration || '').toString(),
|
duration: (item.duration || '').toString(),
|
||||||
severity: (item.severity || '').toString()
|
date: (item.date || '').toString()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user