pediatric-ai-scribe-v3/public/js/encounters.js
Daniel c7f1bd4901 Fix: chart review New clears all fields/notes, date context for AI, Bedrock vendor model-only models
- resetChartReview() clears all pasted notes, visit cards, labs, demographics, and output on New
- clearTab('chart') in encounters.js now calls resetChartReview() for complete reset
- chartReview route injects today's date so AI understands time-relative terms
- Bedrock models updated to vendor model-only with verified 2025 IDs (Sonnet 4, 3.7 Sonnet, 3.5 Sonnet v2, 3.5 Haiku)
- Removed Llama/Mistral/Titan from Bedrock; fixed incorrect vendor-model-4.6 Bedrock IDs
- Bump version to 2.8.0
2026-03-23 18:12:07 +01:00

447 lines
20 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
var _savingInProgress = {}; // prevent duplicate saves on double-click
// Restore saved encounter IDs from sessionStorage (survive page refresh, cleared on tab close)
(function() {
['encounter','dictation','hospital','chart','wellvisit','sickvisit'].forEach(function(t) {
try {
var id = sessionStorage.getItem('_savedEncId_' + t);
if (id) window['_savedEncId_' + t] = id;
} catch(e) {}
});
})();
// 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, onSaved }
if (!opts.label || !opts.label.trim()) { showToast('Enter a patient label first', 'error'); return; }
var type = opts.enc_type || 'encounter';
// Prevent duplicate saves from double-click
if (_savingInProgress[type]) { showToast('Save in progress…', 'info'); return; }
_savingInProgress[type] = true;
fetch('/api/encounters/saved', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify(opts)
})
.then(function(r) { return r.json(); })
.then(function(data) {
_savingInProgress[type] = false;
if (data.success) {
showToast('Encounter saved (' + (opts.label || '') + ')', 'success');
// Persist ID so refreshing the page won't create a duplicate
if (data.id) {
if (opts.onSaved) opts.onSaved(data.id);
try { sessionStorage.setItem('_savedEncId_' + type, data.id); } catch(e) {}
}
loadSavedEncountersList();
} else {
showToast(data.error || 'Save failed', 'error');
}
})
.catch(function() { _savingInProgress[type] = false; 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', 'hosp', 'chart'].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', 'hosp', 'chart'].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', 'hosp', 'chart'].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();
// Map enc_type → DOM element prefix
var domPfxMap = {
encounter: 'enc', dictation: 'dict', hospital: 'hosp', chart: 'chart',
wellvisit: 'wv', sickvisit: 'sick', soap: 'soap', milestones: 'ms'
};
var domPfx = domPfxMap[enc.enc_type] || enc.enc_type;
var noteIdMap = {
encounter: 'enc-hpi-text', dictation: 'dict-hpi-text',
hospital: 'hc-course-text', chart: 'cr-review-text',
wellvisit: 'wv-note-text', sickvisit: 'sick-note-text'
};
// Fill in data
setTimeout(function() {
// Fill label
var labelEl = document.getElementById(domPfx + '-label');
if (labelEl) labelEl.value = enc.label;
// Set saved ID so next save updates same record
window['_savedEncId_' + enc.enc_type] = enc.id;
// Call tab-specific handler
if (window._encLoadHandlers && window._encLoadHandlers[enc.enc_type]) {
window._encLoadHandlers[enc.enc_type](enc);
} else {
// Default: fill transcript and generated note
var transcriptEl = document.getElementById(domPfx + '-transcript');
if (transcriptEl && enc.transcript) transcriptEl.textContent = enc.transcript;
if (enc.generated_note) {
var noteEl = document.getElementById(noteIdMap[enc.enc_type] || (domPfx + '-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) {
// Copy milestones narrative to visit note
if (e.target.closest('#btn-ms-copy-to-note')) {
var msText = document.getElementById('ms-narrative-text');
var wvMsEl = document.getElementById('wv-milestones-text');
if (msText && wvMsEl) {
var content = (msText.innerText || msText.textContent || '').trim();
if (content) {
wvMsEl.value = content;
showToast('Copied! Switching to Visit Note…', 'success');
// Navigate to well visit note subtab so user can see the result
if (typeof window.activateTab === 'function') window.activateTab('wellvisit');
if (typeof window.wvSwitchSubtab === 'function') {
setTimeout(function() { window.wvSwitchSubtab('note'); }, 50);
}
} else {
showToast('Generate a milestone assessment first', 'info');
}
}
}
// 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');
if (e.target.closest('#btn-hosp-save')) saveFromTab('hospital', 'hosp');
if (e.target.closest('#btn-chart-save')) saveFromTab('chart', 'chart');
if (e.target.closest('#btn-wv-save')) saveFromTab('wellvisit', 'wv');
// 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');
if (e.target.closest('#btn-hosp-load')) openLoadPopover('hosp');
if (e.target.closest('#btn-chart-load')) openLoadPopover('chart');
// New Patient / clear tab buttons
if (e.target.closest('#btn-enc-new')) clearTab('encounter');
if (e.target.closest('#btn-dict-new')) clearTab('dictation');
if (e.target.closest('#btn-hosp-new')) clearTab('hospital');
if (e.target.closest('#btn-chart-new')) clearTab('chart');
if (e.target.closest('#btn-wv-new')) clearTab('wellvisit');
if (e.target.closest('#btn-sick-new')) clearTab('sickvisit');
// Close popovers when clicking outside
var loadBtnIds = ['#btn-enc-load','#btn-dict-load','#btn-wv-load','#btn-sick-load','#btn-hosp-load','#btn-chart-load'];
var clickedLoadBtn = loadBtnIds.some(function(id) { return e.target.closest(id); });
if (!clickedLoadBtn) {
['enc', 'dict', 'wv', 'sick', 'hosp', 'chart'].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 label = labelEl ? labelEl.value.trim() : '';
if (!label) { showToast('Enter a patient label before saving', 'error'); return; }
var savedId = window['_savedEncId_' + type] || null;
var transcriptEl = document.getElementById(prefix + '-transcript');
// Each tab stores its generated note in a different element
var noteIdMap = {
encounter: 'enc-hpi-text', dictation: 'dict-hpi-text',
hospital: 'hc-course-text', chart: 'cr-review-text',
wellvisit: 'wv-note-text'
};
var noteEl = document.getElementById(noteIdMap[type] || (prefix + '-hpi-text'));
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; }
});
}
// ── New Patient / Clear tab ────────────────────────────────────────────────
function clearTab(type) {
var pfxMap = { encounter:'enc', dictation:'dict', hospital:'hosp', chart:'chart', wellvisit:'wv', sickvisit:'sick' };
var pfx = pfxMap[type] || type;
var noteElMap = {
encounter:'enc-hpi-text', dictation:'dict-hpi-text',
hospital:'hc-course-text', chart:'cr-review-text',
wellvisit:'wv-note-text', sickvisit:'sick-note-text'
};
var outputElMap = {
encounter:'enc-output', dictation:'dict-output',
hospital:'hc-output', chart:'cr-output',
wellvisit:'wv-note-output', sickvisit:'sick-note-output'
};
// Clear label
var lbl = document.getElementById(pfx + '-label');
if (lbl) lbl.value = '';
// Clear transcript (contenteditable or textarea)
var tr = document.getElementById(pfx + '-transcript');
if (tr) { tr.textContent = ''; tr.innerHTML = ''; }
// Clear generated note
var note = document.getElementById(noteElMap[type]);
if (note) { note.textContent = ''; note.innerHTML = ''; }
// Hide output card
var out = document.getElementById(outputElMap[type]);
if (out) out.classList.add('hidden');
// Reset saved ID (memory + sessionStorage)
window['_savedEncId_' + type] = null;
try { sessionStorage.removeItem('_savedEncId_' + type); } catch(e) {}
// Chart review has additional fields/cards to clear
if (type === 'chart' && typeof window.resetChartReview === 'function') {
window.resetChartReview();
}
showToast('Ready for new patient', 'info');
}
function esc(str) {
if (!str) return '';
return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
console.log('✅ Encounters module loaded');
})();