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:
Daniel Onyejesi 2026-03-22 16:56:28 -04:00
parent d4759fcf58
commit 8317708bb6
5 changed files with 129 additions and 90 deletions

View file

@ -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;}

View file

@ -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 -->

View file

@ -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) {

View file

@ -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. &quot;otitis&quot;, &quot;J06&quot;)..." 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);
});
}

View file

@ -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'"],