- 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)
341 lines
15 KiB
JavaScript
341 lines
15 KiB
JavaScript
// ============================================================
|
|
// ENCOUNTERS.JS — Save/resume/pause encounter progress
|
|
// ============================================================
|
|
|
|
(function() {
|
|
|
|
// ── Pause/Resume for recording buttons ─────────────────────────────────
|
|
// Each recording module (encounter, dictation) gets a pause button wired here.
|
|
|
|
function wirePause(recordBtnId, pauseBtnId, getRecorder) {
|
|
var pauseBtn = document.getElementById(pauseBtnId);
|
|
if (!pauseBtn) return;
|
|
var isPaused = false;
|
|
|
|
pauseBtn.addEventListener('click', function() {
|
|
var rec = getRecorder();
|
|
if (!rec || !rec.mediaRecorder) return;
|
|
if (!isPaused) {
|
|
if (rec.mediaRecorder.state === 'recording') {
|
|
rec.mediaRecorder.pause();
|
|
isPaused = true;
|
|
pauseBtn.innerHTML = '<i class="fas fa-play"></i> Resume';
|
|
pauseBtn.classList.remove('btn-ghost');
|
|
pauseBtn.classList.add('btn-primary');
|
|
}
|
|
} else {
|
|
if (rec.mediaRecorder.state === 'paused') {
|
|
rec.mediaRecorder.resume();
|
|
isPaused = false;
|
|
pauseBtn.innerHTML = '<i class="fas fa-pause"></i> Pause';
|
|
pauseBtn.classList.remove('btn-primary');
|
|
pauseBtn.classList.add('btn-ghost');
|
|
}
|
|
}
|
|
});
|
|
|
|
// Reset when recording stops
|
|
document.addEventListener('recording-stopped', function(e) {
|
|
if (e.detail && e.detail.module === recordBtnId.replace('-record-btn', '')) {
|
|
isPaused = false;
|
|
pauseBtn.innerHTML = '<i class="fas fa-pause"></i> Pause';
|
|
pauseBtn.classList.remove('btn-primary');
|
|
pauseBtn.classList.add('btn-ghost');
|
|
pauseBtn.classList.add('hidden');
|
|
}
|
|
});
|
|
document.addEventListener('recording-started', function(e) {
|
|
if (e.detail && e.detail.module === recordBtnId.replace('-record-btn', '')) {
|
|
isPaused = false;
|
|
pauseBtn.innerHTML = '<i class="fas fa-pause"></i> Pause';
|
|
pauseBtn.classList.remove('btn-primary');
|
|
pauseBtn.classList.add('btn-ghost');
|
|
pauseBtn.classList.remove('hidden');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Expose pause wiring globally (called by each recording module)
|
|
window.wirePauseButton = wirePause;
|
|
|
|
// ── Saved Encounters Manager ───────────────────────────────────────────
|
|
|
|
var _savedEncounters = [];
|
|
var _loadCallback = null; // set by each module to handle loading a saved encounter
|
|
|
|
// Register a load handler for a specific tab type
|
|
window.registerEncounterLoadHandler = function(type, fn) {
|
|
if (!window._encLoadHandlers) window._encLoadHandlers = {};
|
|
window._encLoadHandlers[type] = fn;
|
|
};
|
|
|
|
// Save current encounter state
|
|
window.saveEncounter = function(opts) {
|
|
// opts: { id, label, enc_type, transcript, generated_note, partial_data }
|
|
if (!opts.label || !opts.label.trim()) { showToast('Enter a patient label first', 'error'); return; }
|
|
fetch('/api/encounters/saved', {
|
|
method: 'POST',
|
|
headers: getAuthHeaders(),
|
|
body: JSON.stringify(opts)
|
|
})
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
if (data.success) {
|
|
showToast('Encounter saved (' + (opts.label || '') + ')', 'success');
|
|
// Store the returned ID for subsequent saves
|
|
if (data.id && opts.onSaved) opts.onSaved(data.id);
|
|
loadSavedEncountersList();
|
|
} else {
|
|
showToast(data.error || 'Save failed', 'error');
|
|
}
|
|
})
|
|
.catch(function() { showToast('Save failed', 'error'); });
|
|
};
|
|
|
|
// Load list and refresh all displays (settings page + open popovers)
|
|
function loadSavedEncountersList(targetContainer) {
|
|
fetch('/api/encounters/saved', { headers: getAuthHeaders() })
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
_savedEncounters = data.encounters || [];
|
|
renderSavedList();
|
|
// Refresh any open popovers
|
|
['enc', 'dict', 'wv', 'sick'].forEach(function(pfx) {
|
|
var pop = document.getElementById(pfx + '-load-popover');
|
|
if (pop && !pop.classList.contains('hidden')) {
|
|
var search = pop.querySelector('.enc-load-search');
|
|
renderPopoverList(pfx, search ? search.value : '');
|
|
}
|
|
});
|
|
if (targetContainer) {
|
|
renderPopoverList(targetContainer, '');
|
|
}
|
|
})
|
|
.catch(function() {});
|
|
}
|
|
|
|
function renderPopoverList(pfx, query) {
|
|
var listEl = document.getElementById(pfx + '-pop-list');
|
|
if (!listEl) return;
|
|
var filtered = _savedEncounters.filter(function(enc) {
|
|
if (!query) return true;
|
|
return (enc.label || '').toLowerCase().indexOf(query.toLowerCase()) !== -1 ||
|
|
(enc.enc_type || '').toLowerCase().indexOf(query.toLowerCase()) !== -1;
|
|
});
|
|
if (filtered.length === 0) {
|
|
listEl.innerHTML = '<div style="padding:10px 12px;color:var(--g400);font-size:13px;">' + (query ? 'No matches.' : 'No saved encounters.') + '</div>';
|
|
return;
|
|
}
|
|
listEl.innerHTML = filtered.map(function(enc) {
|
|
var date = enc.updated_at ? new Date(enc.updated_at).toLocaleDateString() : '';
|
|
return '<div class="enc-pop-item" data-id="' + enc.id + '" data-type="' + esc(enc.enc_type) + '">' +
|
|
'<div style="flex:1;min-width:0;">' +
|
|
'<div style="font-weight:600;font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">' + esc(enc.label) + '</div>' +
|
|
'<div style="font-size:11px;color:var(--g500);">' + esc(enc.enc_type) + ' · ' + date + '</div>' +
|
|
'</div>' +
|
|
'<button class="btn-sm btn-ghost enc-pop-delete" data-id="' + enc.id + '" style="color:var(--red);flex-shrink:0;" title="Delete"><i class="fas fa-trash"></i></button>' +
|
|
'</div>';
|
|
}).join('');
|
|
listEl.querySelectorAll('.enc-pop-item').forEach(function(item) {
|
|
item.addEventListener('click', function(e) {
|
|
if (e.target.closest('.enc-pop-delete')) return;
|
|
resumeEncounter(item.dataset.id, item.dataset.type);
|
|
// close popover
|
|
var pop = item.closest('.enc-load-popover');
|
|
if (pop) pop.classList.add('hidden');
|
|
});
|
|
});
|
|
listEl.querySelectorAll('.enc-pop-delete').forEach(function(btn) {
|
|
btn.addEventListener('click', function(e) {
|
|
e.stopPropagation();
|
|
deleteEncounter(btn.dataset.id);
|
|
});
|
|
});
|
|
}
|
|
|
|
function renderSavedList() {
|
|
var container = document.getElementById('saved-enc-list');
|
|
if (!container) return;
|
|
if (_savedEncounters.length === 0) {
|
|
container.innerHTML = '<p style="color:var(--g400);font-size:13px;">No saved encounters.</p>';
|
|
return;
|
|
}
|
|
container.innerHTML = _savedEncounters.map(function(enc) {
|
|
var date = enc.updated_at ? new Date(enc.updated_at).toLocaleDateString() : '';
|
|
var expires = enc.expires_at ? new Date(enc.expires_at).toLocaleDateString() : '';
|
|
var preview = (enc.transcript_preview || '').substring(0, 80);
|
|
return '<div class="saved-enc-item" data-id="' + enc.id + '">' +
|
|
'<div style="flex:1;">' +
|
|
'<div style="display:flex;align-items:center;gap:6px;margin-bottom:2px;">' +
|
|
'<span class="saved-enc-label">' + esc(enc.label) + '</span>' +
|
|
'<span class="saved-enc-type">' + esc(enc.enc_type) + '</span>' +
|
|
'</div>' +
|
|
'<div class="saved-enc-meta">' + date + ' · expires ' + expires + (preview ? ' · ' + esc(preview) + '...' : '') + '</div>' +
|
|
'</div>' +
|
|
'<button class="btn-sm btn-primary enc-resume-btn" data-id="' + enc.id + '" data-type="' + esc(enc.enc_type) + '">Resume</button>' +
|
|
'<button class="btn-sm btn-ghost enc-delete-btn" data-id="' + enc.id + '" style="color:var(--red);"><i class="fas fa-trash"></i></button>' +
|
|
'</div>';
|
|
}).join('');
|
|
|
|
container.querySelectorAll('.enc-resume-btn').forEach(function(btn) {
|
|
btn.addEventListener('click', function() { resumeEncounter(btn.dataset.id, btn.dataset.type); });
|
|
});
|
|
container.querySelectorAll('.enc-delete-btn').forEach(function(btn) {
|
|
btn.addEventListener('click', function() { deleteEncounter(btn.dataset.id); });
|
|
});
|
|
}
|
|
|
|
function openLoadPopover(pfx) {
|
|
var popover = document.getElementById(pfx + '-load-popover');
|
|
if (!popover) return;
|
|
var isHidden = popover.classList.contains('hidden');
|
|
// Close all popovers first
|
|
['enc', 'dict', 'wv', 'sick'].forEach(function(p) {
|
|
var pop = document.getElementById(p + '-load-popover');
|
|
if (pop) pop.classList.add('hidden');
|
|
});
|
|
if (!isHidden) return; // was open, now closed
|
|
popover.classList.remove('hidden');
|
|
var searchEl = popover.querySelector('.enc-load-search');
|
|
if (searchEl) { searchEl.value = ''; searchEl.focus(); }
|
|
if (_savedEncounters.length === 0) {
|
|
loadSavedEncountersList();
|
|
} else {
|
|
renderPopoverList(pfx, '');
|
|
}
|
|
// Wire search
|
|
if (searchEl && !searchEl._wired) {
|
|
searchEl._wired = true;
|
|
searchEl.addEventListener('input', function() { renderPopoverList(pfx, this.value); });
|
|
}
|
|
// Wire close button — stop propagation so doc handler doesn't re-process
|
|
var closeBtn = popover.querySelector('.enc-pop-close');
|
|
if (closeBtn && !closeBtn._wired) {
|
|
closeBtn._wired = true;
|
|
closeBtn.addEventListener('click', function(e) { e.stopPropagation(); popover.classList.add('hidden'); });
|
|
}
|
|
// Stop click propagation inside popover so outside-click handler doesn't fire for inner clicks
|
|
if (!popover._stopPropWired) {
|
|
popover._stopPropWired = true;
|
|
popover.addEventListener('click', function(e) { e.stopPropagation(); });
|
|
}
|
|
}
|
|
|
|
function resumeEncounter(id, type) {
|
|
fetch('/api/encounters/saved/' + id, { headers: getAuthHeaders() })
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
if (!data.success) { showToast(data.error || 'Failed', 'error'); return; }
|
|
var enc = data.encounter;
|
|
// Close all popovers
|
|
['enc', 'dict', 'wv', 'sick'].forEach(function(p) {
|
|
var pop = document.getElementById(p + '-load-popover');
|
|
if (pop) pop.classList.add('hidden');
|
|
});
|
|
|
|
// Navigate to correct tab
|
|
var tabMap = {
|
|
encounter: 'encounter', dictation: 'dictation', hospital: 'hospital',
|
|
chart: 'chart', soap: 'soap', milestones: 'milestones',
|
|
wellvisit: 'wellvisit', sickvisit: 'sickvisit'
|
|
};
|
|
var tabName = tabMap[enc.enc_type] || 'encounter';
|
|
var tabBtn = document.querySelector('.tab-btn[data-tab="' + tabName + '"]');
|
|
if (tabBtn) tabBtn.click();
|
|
|
|
// Fill in data
|
|
setTimeout(function() {
|
|
// Fill label
|
|
var labelEl = document.getElementById(tabName + '-label') || document.getElementById(enc.enc_type + '-label');
|
|
if (labelEl) labelEl.value = enc.label;
|
|
|
|
// Set saved ID so next save updates same record
|
|
window['_savedEncId_' + tabName] = enc.id;
|
|
|
|
// Call tab-specific handler
|
|
if (window._encLoadHandlers && window._encLoadHandlers[enc.enc_type]) {
|
|
window._encLoadHandlers[enc.enc_type](enc);
|
|
} else {
|
|
// Default: fill transcript field if it exists
|
|
var transcriptEl = document.getElementById(tabName + '-transcript') || document.getElementById(enc.enc_type + '-transcript');
|
|
if (transcriptEl && enc.transcript) transcriptEl.textContent = enc.transcript;
|
|
// Fill generated note
|
|
if (enc.generated_note) {
|
|
var noteEl = document.getElementById(enc.enc_type + '-hpi-text') || document.getElementById(tabName + '-hpi-text');
|
|
if (noteEl) {
|
|
noteEl.textContent = enc.generated_note;
|
|
var outputCard = noteEl.closest('.output-card');
|
|
if (outputCard) outputCard.classList.remove('hidden');
|
|
}
|
|
}
|
|
}
|
|
showToast('Encounter "' + enc.label + '" loaded', 'success');
|
|
}, 200);
|
|
})
|
|
.catch(function() { showToast('Load failed', 'error'); });
|
|
}
|
|
|
|
function deleteEncounter(id) {
|
|
fetch('/api/encounters/saved/' + id, { method: 'DELETE', headers: getAuthHeaders() })
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
if (data.success) { showToast('Deleted', 'info'); loadSavedEncountersList(); }
|
|
else showToast(data.error || 'Delete failed', 'error');
|
|
})
|
|
.catch(function() { showToast('Delete failed', 'error'); });
|
|
}
|
|
|
|
// Expose loadSavedEncountersList globally so auth.js / settings page can call it
|
|
window.loadSavedEncountersList = loadSavedEncountersList;
|
|
|
|
document.addEventListener('click', function(e) {
|
|
// Settings button — refresh saved encounters list
|
|
if (e.target.closest('#btn-settings')) {
|
|
loadSavedEncountersList();
|
|
}
|
|
// Save buttons per tab
|
|
if (e.target.closest('#btn-enc-save')) saveFromTab('encounter', 'enc');
|
|
if (e.target.closest('#btn-dict-save')) saveFromTab('dictation', 'dict');
|
|
// Load buttons — open inline popovers
|
|
if (e.target.closest('#btn-enc-load')) openLoadPopover('enc');
|
|
if (e.target.closest('#btn-dict-load')) openLoadPopover('dict');
|
|
if (e.target.closest('#btn-wv-load')) openLoadPopover('wv');
|
|
if (e.target.closest('#btn-sick-load')) openLoadPopover('sick');
|
|
// Close popovers when clicking outside (popover stops its own propagation so this only fires for outside clicks)
|
|
if (!e.target.closest('#btn-enc-load') && !e.target.closest('#btn-dict-load') &&
|
|
!e.target.closest('#btn-wv-load') && !e.target.closest('#btn-sick-load')) {
|
|
['enc', 'dict', 'wv', 'sick'].forEach(function(p) {
|
|
var pop = document.getElementById(p + '-load-popover');
|
|
if (pop) pop.classList.add('hidden');
|
|
});
|
|
}
|
|
});
|
|
|
|
function saveFromTab(type, prefix) {
|
|
var labelEl = document.getElementById(prefix + '-label');
|
|
var transcriptEl = document.getElementById(prefix + '-transcript');
|
|
var noteEl = document.getElementById(prefix + '-hpi-text');
|
|
var label = labelEl ? labelEl.value.trim() : '';
|
|
if (!label) { showToast('Enter a patient label before saving', 'error'); return; }
|
|
|
|
var savedId = window['_savedEncId_' + type] || null;
|
|
|
|
window.saveEncounter({
|
|
id: savedId,
|
|
label: label,
|
|
enc_type: type,
|
|
transcript: transcriptEl ? (transcriptEl.innerText || transcriptEl.textContent || '') : '',
|
|
generated_note: noteEl ? (noteEl.innerText || noteEl.textContent || '') : '',
|
|
onSaved: function(newId) { window['_savedEncId_' + type] = newId; }
|
|
});
|
|
}
|
|
|
|
function esc(str) {
|
|
if (!str) return '';
|
|
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
}
|
|
|
|
|
|
console.log('✅ Encounters module loaded');
|
|
|
|
})();
|