Fix: load popover positioning, delete button, ICD-10 live search
- Wrap save-bar + popover in .save-bar-wrap (position:relative) so the absolute-positioned popover anchors correctly below the Load button (previously popovers were outside the save-bar, so had no positioned ancestor and appeared at the wrong place on screen) - Remove confirm() dialog from deleteEncounter — encounter disappears immediately from the popover list after deletion - ICD-10 diagnoses: replace static-only lookup with live NLM Clinical Tables API search (clinicaltables.nlm.nih.gov) with 280ms debounce; results appear in a dropdown below the search input; Enter/click selects - Add clinicaltables.nlm.nih.gov to CSP connect-src - Drop unused position:relative from .save-bar (now on .save-bar-wrap)
This commit is contained in:
parent
d4759fcf58
commit
8317708bb6
5 changed files with 129 additions and 90 deletions
|
|
@ -358,7 +358,8 @@ body{font-family:'Inter',system-ui,sans-serif;background:var(--g50);color:var(--
|
|||
.announcement-close:hover{opacity:1;}
|
||||
|
||||
/* Save bar (encounter label + save/load) */
|
||||
.save-bar{display:flex;align-items:center;gap:8px;padding:8px 12px;background:var(--g50);border:1px solid var(--g200);border-radius:var(--radius);margin-bottom:10px;flex-wrap:wrap;position:relative;}
|
||||
.save-bar-wrap{position:relative;margin-bottom:10px;}
|
||||
.save-bar{display:flex;align-items:center;gap:8px;padding:8px 12px;background:var(--g50);border:1px solid var(--g200);border-radius:var(--radius);flex-wrap:wrap;}
|
||||
.save-label-input{flex:1;min-width:160px;font-size:13px;padding:5px 8px;border:1px solid var(--g200);border-radius:6px;background:white;}
|
||||
.save-label-input:focus{outline:none;border-color:var(--blue);box-shadow:0 0 0 2px rgba(37,99,235,.12);}
|
||||
|
||||
|
|
@ -440,3 +441,10 @@ body{font-family:'Inter',system-ui,sans-serif;background:var(--g50);color:var(--
|
|||
.ros-note-input{width:100%;}
|
||||
.dx-chip{font-size:10px;}
|
||||
}
|
||||
|
||||
/* ICD-10 live search dropdown */
|
||||
.dx-results{position:absolute;top:calc(100% + 2px);left:0;right:0;background:white;border:1px solid var(--g300);border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,0.12);z-index:400;max-height:240px;overflow-y:auto;}
|
||||
.dx-result-item{padding:7px 12px;cursor:pointer;font-size:13px;border-bottom:1px solid var(--g100);}
|
||||
.dx-result-item:last-child{border-bottom:none;}
|
||||
.dx-result-item:hover{background:var(--g50);}
|
||||
.dx-result-code{font-weight:700;color:var(--blue);margin-right:4px;}
|
||||
|
|
|
|||
|
|
@ -212,20 +212,22 @@
|
|||
</div>
|
||||
|
||||
<!-- Save/Load bar -->
|
||||
<div class="save-bar" id="enc-save-bar">
|
||||
<div style="display:flex;align-items:center;gap:8px;flex:1;">
|
||||
<i class="fas fa-tag" style="color:var(--g400);font-size:13px;"></i>
|
||||
<input type="text" id="enc-label" class="save-label-input" placeholder="Patient label (e.g. John D, Visit #42)">
|
||||
<div class="save-bar-wrap">
|
||||
<div class="save-bar" id="enc-save-bar">
|
||||
<div style="display:flex;align-items:center;gap:8px;flex:1;">
|
||||
<i class="fas fa-tag" style="color:var(--g400);font-size:13px;"></i>
|
||||
<input type="text" id="enc-label" class="save-label-input" placeholder="Patient label (e.g. John D, Visit #42)">
|
||||
</div>
|
||||
<button id="btn-enc-save" class="btn-sm btn-ghost"><i class="fas fa-floppy-disk"></i> Save</button>
|
||||
<button id="btn-enc-load" class="btn-sm btn-ghost"><i class="fas fa-folder-open"></i> Load</button>
|
||||
</div>
|
||||
<button id="btn-enc-save" class="btn-sm btn-ghost"><i class="fas fa-floppy-disk"></i> Save</button>
|
||||
<button id="btn-enc-load" class="btn-sm btn-ghost"><i class="fas fa-folder-open"></i> Load</button>
|
||||
</div>
|
||||
<div id="enc-load-popover" class="enc-load-popover hidden">
|
||||
<div class="enc-load-popover-inner">
|
||||
<input type="text" id="enc-load-search" class="enc-load-search" placeholder="Search saved encounters...">
|
||||
<button class="enc-pop-close btn-sm btn-ghost"><i class="fas fa-times"></i></button>
|
||||
<div id="enc-load-popover" class="enc-load-popover hidden">
|
||||
<div class="enc-load-popover-inner">
|
||||
<input type="text" id="enc-load-search" class="enc-load-search" placeholder="Search saved encounters...">
|
||||
<button class="enc-pop-close btn-sm btn-ghost"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<div id="enc-pop-list" class="enc-pop-list"></div>
|
||||
</div>
|
||||
<div id="enc-pop-list" class="enc-pop-list"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
|
|
@ -287,20 +289,22 @@
|
|||
</div>
|
||||
|
||||
<!-- Save/Load bar -->
|
||||
<div class="save-bar" id="dict-save-bar">
|
||||
<div style="display:flex;align-items:center;gap:8px;flex:1;">
|
||||
<i class="fas fa-tag" style="color:var(--g400);font-size:13px;"></i>
|
||||
<input type="text" id="dict-label" class="save-label-input" placeholder="Patient label (e.g. John D, Visit #42)">
|
||||
<div class="save-bar-wrap">
|
||||
<div class="save-bar" id="dict-save-bar">
|
||||
<div style="display:flex;align-items:center;gap:8px;flex:1;">
|
||||
<i class="fas fa-tag" style="color:var(--g400);font-size:13px;"></i>
|
||||
<input type="text" id="dict-label" class="save-label-input" placeholder="Patient label (e.g. John D, Visit #42)">
|
||||
</div>
|
||||
<button id="btn-dict-save" class="btn-sm btn-ghost"><i class="fas fa-floppy-disk"></i> Save</button>
|
||||
<button id="btn-dict-load" class="btn-sm btn-ghost"><i class="fas fa-folder-open"></i> Load</button>
|
||||
</div>
|
||||
<button id="btn-dict-save" class="btn-sm btn-ghost"><i class="fas fa-floppy-disk"></i> Save</button>
|
||||
<button id="btn-dict-load" class="btn-sm btn-ghost"><i class="fas fa-folder-open"></i> Load</button>
|
||||
</div>
|
||||
<div id="dict-load-popover" class="enc-load-popover hidden">
|
||||
<div class="enc-load-popover-inner">
|
||||
<input type="text" id="dict-load-search" class="enc-load-search" placeholder="Search saved encounters...">
|
||||
<button class="enc-pop-close btn-sm btn-ghost"><i class="fas fa-times"></i></button>
|
||||
<div id="dict-load-popover" class="enc-load-popover hidden">
|
||||
<div class="enc-load-popover-inner">
|
||||
<input type="text" id="dict-load-search" class="enc-load-search" placeholder="Search saved encounters...">
|
||||
<button class="enc-pop-close btn-sm btn-ghost"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<div id="dict-pop-list" class="enc-pop-list"></div>
|
||||
</div>
|
||||
<div id="dict-pop-list" class="enc-pop-list"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
|
|
@ -1041,20 +1045,22 @@
|
|||
<div id="wv-panel-note" class="wv-subpanel hidden">
|
||||
|
||||
<!-- Save/Load bar -->
|
||||
<div class="save-bar" id="wv-save-bar">
|
||||
<div style="display:flex;align-items:center;gap:8px;flex:1;">
|
||||
<i class="fas fa-tag" style="color:var(--g400);font-size:13px;"></i>
|
||||
<input type="text" id="wv-label" class="save-label-input" placeholder="Patient label (e.g. Jane D, Visit #7)">
|
||||
<div class="save-bar-wrap">
|
||||
<div class="save-bar" id="wv-save-bar">
|
||||
<div style="display:flex;align-items:center;gap:8px;flex:1;">
|
||||
<i class="fas fa-tag" style="color:var(--g400);font-size:13px;"></i>
|
||||
<input type="text" id="wv-label" class="save-label-input" placeholder="Patient label (e.g. Jane D, Visit #7)">
|
||||
</div>
|
||||
<button id="btn-wv-save" class="btn-sm btn-ghost"><i class="fas fa-floppy-disk"></i> Save</button>
|
||||
<button id="btn-wv-load" class="btn-sm btn-ghost"><i class="fas fa-folder-open"></i> Load</button>
|
||||
</div>
|
||||
<button id="btn-wv-save" class="btn-sm btn-ghost"><i class="fas fa-floppy-disk"></i> Save</button>
|
||||
<button id="btn-wv-load" class="btn-sm btn-ghost"><i class="fas fa-folder-open"></i> Load</button>
|
||||
</div>
|
||||
<div id="wv-load-popover" class="enc-load-popover hidden">
|
||||
<div class="enc-load-popover-inner">
|
||||
<input type="text" id="wv-load-search" class="enc-load-search" placeholder="Search saved encounters...">
|
||||
<button class="enc-pop-close btn-sm btn-ghost"><i class="fas fa-times"></i></button>
|
||||
<div id="wv-load-popover" class="enc-load-popover hidden">
|
||||
<div class="enc-load-popover-inner">
|
||||
<input type="text" id="wv-load-search" class="enc-load-search" placeholder="Search saved encounters...">
|
||||
<button class="enc-pop-close btn-sm btn-ghost"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<div id="wv-pop-list" class="enc-pop-list"></div>
|
||||
</div>
|
||||
<div id="wv-pop-list" class="enc-pop-list"></div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom:10px;">
|
||||
|
|
@ -1177,20 +1183,22 @@
|
|||
</div>
|
||||
|
||||
<!-- Save/Load bar -->
|
||||
<div class="save-bar" id="sick-save-bar">
|
||||
<div style="display:flex;align-items:center;gap:8px;flex:1;">
|
||||
<i class="fas fa-tag" style="color:var(--g400);font-size:13px;"></i>
|
||||
<input type="text" id="sick-label" class="save-label-input" placeholder="Patient label (e.g. John D, 5y fever)">
|
||||
<div class="save-bar-wrap">
|
||||
<div class="save-bar" id="sick-save-bar">
|
||||
<div style="display:flex;align-items:center;gap:8px;flex:1;">
|
||||
<i class="fas fa-tag" style="color:var(--g400);font-size:13px;"></i>
|
||||
<input type="text" id="sick-label" class="save-label-input" placeholder="Patient label (e.g. John D, 5y fever)">
|
||||
</div>
|
||||
<button id="btn-sick-save" class="btn-sm btn-ghost"><i class="fas fa-floppy-disk"></i> Save</button>
|
||||
<button id="btn-sick-load" class="btn-sm btn-ghost"><i class="fas fa-folder-open"></i> Load</button>
|
||||
</div>
|
||||
<button id="btn-sick-save" class="btn-sm btn-ghost"><i class="fas fa-floppy-disk"></i> Save</button>
|
||||
<button id="btn-sick-load" class="btn-sm btn-ghost"><i class="fas fa-folder-open"></i> Load</button>
|
||||
</div>
|
||||
<div id="sick-load-popover" class="enc-load-popover hidden">
|
||||
<div class="enc-load-popover-inner">
|
||||
<input type="text" id="sick-load-search" class="enc-load-search" placeholder="Search saved encounters...">
|
||||
<button class="enc-pop-close btn-sm btn-ghost"><i class="fas fa-times"></i></button>
|
||||
<div id="sick-load-popover" class="enc-load-popover hidden">
|
||||
<div class="enc-load-popover-inner">
|
||||
<input type="text" id="sick-load-search" class="enc-load-search" placeholder="Search saved encounters...">
|
||||
<button class="enc-pop-close btn-sm btn-ghost"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<div id="sick-pop-list" class="enc-pop-list"></div>
|
||||
</div>
|
||||
<div id="sick-pop-list" class="enc-pop-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- Demographics -->
|
||||
|
|
|
|||
|
|
@ -276,7 +276,6 @@
|
|||
}
|
||||
|
||||
function deleteEncounter(id) {
|
||||
if (!confirm('Delete this saved encounter? This cannot be undone.')) return;
|
||||
fetch('/api/encounters/saved/' + id, { method: 'DELETE', headers: getAuthHeaders() })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
|
|
|
|||
|
|
@ -541,31 +541,43 @@
|
|||
// Expose so sick visit can reuse
|
||||
window.COMMON_DX = COMMON_DX;
|
||||
|
||||
// ICD-10 live search via NLM Clinical Tables API (free, no auth, CORS-enabled)
|
||||
var _icdSearchTimer = {};
|
||||
function searchIcd10(query, onResults) {
|
||||
if (!query || query.length < 2) { onResults([]); return; }
|
||||
fetch('https://clinicaltables.nlm.nih.gov/api/icd10cm/v3/search?sf=code,name&terms=' + encodeURIComponent(query) + '&maxList=12')
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
// data = [count, [codes], null, [[code, name], ...]]
|
||||
var items = (data[3] || []).map(function(row) { return { code: row[0], name: row[1] }; });
|
||||
onResults(items);
|
||||
})
|
||||
.catch(function() { onResults([]); });
|
||||
}
|
||||
|
||||
// Render the DX component into a container element
|
||||
// selectedDxArr = reference to an array of { code, name }
|
||||
// idPrefix = unique prefix for element IDs (e.g. 'wv' or 'sv')
|
||||
window.renderDxComponent = function(containerEl, selectedDxArr, idPrefix) {
|
||||
var searchId = idPrefix + '-dx-search';
|
||||
var tagsId = idPrefix + '-dx-tags';
|
||||
var chipsId = idPrefix + '-dx-chips';
|
||||
var freetextId = idPrefix + '-dx-freetext';
|
||||
var searchId = idPrefix + '-dx-search';
|
||||
var tagsId = idPrefix + '-dx-tags';
|
||||
var resultsId = idPrefix + '-dx-results';
|
||||
var chipsId = idPrefix + '-dx-chips';
|
||||
|
||||
containerEl.innerHTML =
|
||||
'<div style="margin-bottom:8px;">' +
|
||||
'<div style="display:flex;gap:6px;align-items:center;margin-bottom:6px;">' +
|
||||
'<input id="' + searchId + '" type="text" placeholder="Search or type ICD-10 code..." style="flex:1;font-size:12px;padding:6px 10px;border:1px solid var(--g300);border-radius:6px;font-family:inherit;">' +
|
||||
'<button class="btn-sm btn-primary" id="' + idPrefix + '-dx-add-btn" style="font-size:12px;white-space:nowrap;"><i class="fas fa-plus"></i> Add</button>' +
|
||||
'<div style="position:relative;margin-bottom:8px;">' +
|
||||
'<div style="display:flex;gap:6px;align-items:center;">' +
|
||||
'<div style="flex:1;position:relative;">' +
|
||||
'<input id="' + searchId + '" type="text" placeholder="Search ICD-10 (e.g. "otitis", "J06")..." autocomplete="off" style="width:100%;font-size:13px;padding:7px 10px;border:1px solid var(--g300);border-radius:8px;font-family:inherit;box-sizing:border-box;">' +
|
||||
'<div id="' + resultsId + '" class="dx-results hidden"></div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div id="' + tagsId + '" style="display:flex;flex-wrap:wrap;gap:4px;min-height:28px;margin-bottom:6px;"></div>' +
|
||||
'<div style="font-size:11px;color:var(--g500);margin-bottom:4px;">Common diagnoses — click to add:</div>' +
|
||||
'<div id="' + tagsId + '" style="display:flex;flex-wrap:wrap;gap:4px;margin-top:8px;min-height:24px;"></div>' +
|
||||
'<div style="font-size:11px;color:var(--g500);margin:8px 0 4px;">Common pediatric diagnoses:</div>' +
|
||||
'<div id="' + chipsId + '" style="display:flex;flex-wrap:wrap;gap:4px;"></div>' +
|
||||
'</div>' +
|
||||
'<div style="margin-top:8px;">' +
|
||||
'<label style="font-size:12px;font-weight:600;color:var(--g600);display:block;margin-bottom:4px;">Additional diagnoses (free text):</label>' +
|
||||
'<input id="' + freetextId + '" type="text" placeholder="e.g. Asthma J45.20, GERD K21.9" style="width:100%;font-size:12px;padding:6px 10px;border:1px solid var(--g300);border-radius:6px;font-family:inherit;">' +
|
||||
'</div>';
|
||||
|
||||
// Render chips
|
||||
// Render quick-access chips
|
||||
var chipsEl = containerEl.querySelector('#' + chipsId);
|
||||
if (chipsEl) {
|
||||
chipsEl.innerHTML = COMMON_DX.map(function(dx) {
|
||||
|
|
@ -578,35 +590,47 @@
|
|||
});
|
||||
}
|
||||
|
||||
// Add button / Enter key
|
||||
// Live search
|
||||
var searchEl = containerEl.querySelector('#' + searchId);
|
||||
var addBtn = containerEl.querySelector('#' + idPrefix + '-dx-add-btn');
|
||||
function addFromSearch() {
|
||||
var val = searchEl ? searchEl.value.trim() : '';
|
||||
if (!val) return;
|
||||
// Try to match a common dx
|
||||
var match = COMMON_DX.find(function(dx) {
|
||||
return dx.code.toLowerCase() === val.toLowerCase() ||
|
||||
dx.name.toLowerCase().indexOf(val.toLowerCase()) !== -1 ||
|
||||
val.toLowerCase().indexOf(dx.code.toLowerCase()) !== -1;
|
||||
var resultsEl = containerEl.querySelector('#' + resultsId);
|
||||
|
||||
function showResults(items) {
|
||||
if (!items.length) { resultsEl.classList.add('hidden'); return; }
|
||||
resultsEl.innerHTML = items.map(function(dx) {
|
||||
return '<div class="dx-result-item" data-code="' + esc(dx.code) + '" data-name="' + esc(dx.name) + '">' +
|
||||
'<span class="dx-result-code">' + esc(dx.code) + '</span> ' + esc(dx.name) +
|
||||
'</div>';
|
||||
}).join('');
|
||||
resultsEl.classList.remove('hidden');
|
||||
resultsEl.querySelectorAll('.dx-result-item').forEach(function(item) {
|
||||
item.addEventListener('mousedown', function(e) {
|
||||
e.preventDefault(); // prevent blur from firing before click
|
||||
addDx(selectedDxArr, { code: item.dataset.code, name: item.dataset.name }, containerEl, tagsId);
|
||||
searchEl.value = '';
|
||||
resultsEl.classList.add('hidden');
|
||||
});
|
||||
});
|
||||
if (match) {
|
||||
addDx(selectedDxArr, match, containerEl, tagsId);
|
||||
} else {
|
||||
// Parse as "Name CODE" or just free text
|
||||
var parts = val.match(/^(.*?)\s+([A-Z][0-9][0-9A-Z\.]*)$/i);
|
||||
if (parts) {
|
||||
addDx(selectedDxArr, { code: parts[2].toUpperCase(), name: parts[1].trim() }, containerEl, tagsId);
|
||||
} else {
|
||||
addDx(selectedDxArr, { code: '', name: val }, containerEl, tagsId);
|
||||
}
|
||||
}
|
||||
if (searchEl) searchEl.value = '';
|
||||
}
|
||||
if (addBtn) addBtn.addEventListener('click', addFromSearch);
|
||||
|
||||
if (searchEl) {
|
||||
searchEl.addEventListener('input', function() {
|
||||
var val = searchEl.value.trim();
|
||||
clearTimeout(_icdSearchTimer[idPrefix]);
|
||||
if (!val) { resultsEl.classList.add('hidden'); return; }
|
||||
_icdSearchTimer[idPrefix] = setTimeout(function() {
|
||||
searchIcd10(val, showResults);
|
||||
}, 280);
|
||||
});
|
||||
searchEl.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter') { e.preventDefault(); addFromSearch(); }
|
||||
if (e.key === 'Escape') { resultsEl.classList.add('hidden'); }
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
var first = resultsEl.querySelector('.dx-result-item');
|
||||
if (first) { first.dispatchEvent(new MouseEvent('mousedown')); }
|
||||
}
|
||||
});
|
||||
searchEl.addEventListener('blur', function() {
|
||||
setTimeout(function() { resultsEl.classList.add('hidden'); }, 150);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ app.use(helmet({
|
|||
fontSrc: ["'self'", 'https://fonts.gstatic.com', 'https://cdnjs.cloudflare.com'],
|
||||
imgSrc: ["'self'", 'data:', 'blob:'],
|
||||
mediaSrc: ["'self'", 'blob:'],
|
||||
connectSrc: ["'self'", 'https://cdnjs.cloudflare.com', 'https://fonts.googleapis.com', 'https://fonts.gstatic.com', 'https://www.google.com', 'wss://www.google.com'],
|
||||
connectSrc: ["'self'", 'https://cdnjs.cloudflare.com', 'https://fonts.googleapis.com', 'https://fonts.gstatic.com', 'https://www.google.com', 'wss://www.google.com', 'https://clinicaltables.nlm.nih.gov'],
|
||||
workerSrc: ["'self'"],
|
||||
frameSrc: ["'none'"],
|
||||
objectSrc: ["'none'"],
|
||||
|
|
|
|||
Loading…
Reference in a new issue