Retrying a kept recording transcribed it and put the text on the clipboard, leaving you to find the right tab and paste. The app already had the answer: the module was recorded with the audio. Retry now opens that tab and puts the text in its transcript box. Two things had to be true first. The module was not actually being recorded. transcribeAudio never sent one, so the server stored its default for every upload — all 28 rows in audio_backups said "recording", and a retry had nowhere to send anything back to. Each module's call now tags its own upload. And the names disagreed. The recorders tagged 'encounter', 'soap', 'dictation' while the recording-started events said 'enc', 'sick', 'dict'. One table now holds the mapping and resolves the aliases, so the recorder that tags the upload, the backup row that labels it and the retry that delivers it cannot drift apart again. Existing text is appended to, never replaced: a retry usually recovers something on top of a live transcript, and overwriting would lose the words the browser did hear. The box only exists once its tab's markup has been fetched, so delivery polls briefly rather than guessing a delay, and falls back to the clipboard if the tab never opens. An empty result says so rather than claiming success. Backup rows now name their source and the button reads "Retry into SOAP Note" instead of "Retry". Verified in a browser: enc resolves to encounter, delivery switched tabs and produced 'existing live transcript\n\nRECOVERED TEXT'. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
653 lines
28 KiB
JavaScript
653 lines
28 KiB
JavaScript
// ============================================================
|
|
// ED ENCOUNTER TAB — multi-stage emergency note with per-stage
|
|
// don't-miss tooltips and 2023 E/M MDM finalize.
|
|
//
|
|
// State model:
|
|
// _state = {
|
|
// stage: 1|2|..., // current stage number (what the recorder is for)
|
|
// stages: [ // per-stage history — one entry per generated stage
|
|
// { transcript, note, dontMiss, model, generatedAt }
|
|
// ],
|
|
// finalized: false,
|
|
// finalNote: null, // consolidated final note from /finalize
|
|
// mdm: null
|
|
// }
|
|
//
|
|
// UX rules:
|
|
// - Every generated stage stays on screen as its own editable card with
|
|
// its own don't-miss panel. The physician can edit any stage at any
|
|
// time; gatherCurrentNotes() reads the live DOM into _state.stages
|
|
// before any operation that depends on the current text.
|
|
// - The badge under the patient label reflects exactly where you are:
|
|
// "Stage N (recording)" — advanced to stage N but no note yet
|
|
// "Stage N" — stage N's note has been generated
|
|
// "Finalized" — Save & Done done
|
|
// - Save & Done sends every stage's current text to the server, which
|
|
// consolidates them into one polished final note and then generates
|
|
// MDM from that. Stages become read-only after finalize.
|
|
//
|
|
// Persistence: localStorage on every input (debounced) + auto-save
|
|
// draft row to saved_encounters on stage transitions.
|
|
// ============================================================
|
|
|
|
var LS_KEY = 'ped_ed_draft_v1';
|
|
var boundary = window.AccountBoundary;
|
|
|
|
var _state = freshState();
|
|
var _saveTimer = null;
|
|
var _recorder = null;
|
|
var _timer = null;
|
|
var _recording = false;
|
|
var _paused = false;
|
|
var _recognition = null;
|
|
var _liveTranscript = '';
|
|
|
|
function freshState() {
|
|
return { stage: 1, stages: [], finalized: false, finalNote: null, mdm: null };
|
|
}
|
|
|
|
// ── DOM helpers ──────────────────────────────────────────────────────
|
|
function getVal(id) { var el = document.getElementById(id); return el ? el.value : ''; }
|
|
function setVal(id, v) { var el = document.getElementById(id); if (el) el.value = v; }
|
|
function getText(id) {
|
|
var el = document.getElementById(id);
|
|
if (!el) return '';
|
|
return (el.innerText || el.textContent || '').trim();
|
|
}
|
|
function setText(id, v) {
|
|
var el = document.getElementById(id);
|
|
if (!el) return;
|
|
el.textContent = v;
|
|
}
|
|
function showEl(id) { var el = document.getElementById(id); if (el) el.classList.remove('hidden'); }
|
|
function hideEl(id) { var el = document.getElementById(id); if (el) el.classList.add('hidden'); }
|
|
function escHtml(s) { return String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); }
|
|
|
|
function stageTextElId(idx) { return 'ed-stage-text-' + idx; }
|
|
|
|
// Read the current contenteditable text from each rendered stage card
|
|
// and write it back into _state.stages. Call before any operation
|
|
// (advance, finalize, persist) that needs the latest user edits.
|
|
function gatherCurrentNotes() {
|
|
for (var i = 0; i < _state.stages.length; i++) {
|
|
var el = document.getElementById(stageTextElId(i));
|
|
if (el) {
|
|
var t = (el.innerText || el.textContent || '').trim();
|
|
if (t) _state.stages[i].note = t;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Persistence ──────────────────────────────────────────────────────
|
|
function persistLocal() {
|
|
var owner = boundary.capture();
|
|
if (!owner) return;
|
|
if (_saveTimer) clearTimeout(_saveTimer);
|
|
_saveTimer = setTimeout(function() {
|
|
if (!boundary.valid(owner)) return;
|
|
try {
|
|
gatherCurrentNotes();
|
|
var snap = {
|
|
state: _state,
|
|
label: getVal('ed-label'),
|
|
age: getVal('ed-age'),
|
|
gender: getVal('ed-gender'),
|
|
cc: getVal('ed-cc'),
|
|
currentTranscript: getText('ed-transcript')
|
|
};
|
|
localStorage.setItem(boundary.storageKey(LS_KEY), JSON.stringify(snap));
|
|
} catch (e) {}
|
|
}, 300);
|
|
}
|
|
|
|
function loadLocal() {
|
|
try {
|
|
var raw = localStorage.getItem(boundary.storageKey(LS_KEY));
|
|
if (!raw) return;
|
|
var snap = JSON.parse(raw);
|
|
if (!snap || !snap.state) return;
|
|
_state = snap.state;
|
|
// Backfill new fields if a snapshot pre-dates this version
|
|
if (typeof _state.finalNote === 'undefined') _state.finalNote = null;
|
|
setVal('ed-label', snap.label || '');
|
|
setVal('ed-age', snap.age || '');
|
|
setVal('ed-gender', snap.gender || '');
|
|
setVal('ed-cc', snap.cc || '');
|
|
setText('ed-transcript', snap.currentTranscript || '');
|
|
renderStages();
|
|
if (_state.finalNote) renderFinalNote(_state.finalNote);
|
|
if (_state.finalized && _state.mdm) renderMdm(_state.mdm);
|
|
updateBadge();
|
|
} catch (e) {}
|
|
}
|
|
|
|
function clearLocal() {
|
|
if (!boundary.active()) return;
|
|
try { localStorage.removeItem(boundary.storageKey(LS_KEY)); } catch (e) {}
|
|
}
|
|
|
|
// ── Stage rendering ──────────────────────────────────────────────────
|
|
function renderStages() {
|
|
var container = document.getElementById('ed-stages-container');
|
|
if (!container) return;
|
|
container.innerHTML = '';
|
|
if (_state.stages.length === 0) {
|
|
hideEl('ed-tail-controls');
|
|
return;
|
|
}
|
|
for (var i = 0; i < _state.stages.length; i++) {
|
|
container.appendChild(buildStageCard(i, _state.stages[i]));
|
|
}
|
|
// Tail controls visible once at least one stage exists.
|
|
// Hidden when finalized — encounter is locked.
|
|
if (_state.finalized) hideEl('ed-tail-controls');
|
|
else showEl('ed-tail-controls');
|
|
}
|
|
|
|
function buildStageCard(idx, stage) {
|
|
var card = document.createElement('div');
|
|
card.className = 'card output-card stage-card';
|
|
card.dataset.stageIdx = String(idx);
|
|
card.style.marginBottom = '10px';
|
|
var modelShort = (stage.model || '').split('/').pop();
|
|
var editable = _state.finalized ? 'false' : 'true';
|
|
var dontMissHtml = '';
|
|
if (stage.dontMiss && stage.dontMiss.length) {
|
|
dontMissHtml =
|
|
'<div style="border-top:1px solid var(--g100);padding:10px 14px;background:#fffbeb;">' +
|
|
'<div style="font-size:12px;font-weight:700;color:#92400e;margin-bottom:6px;">' +
|
|
'<i class="fas fa-triangle-exclamation" style="color:#f59e0b;margin-right:6px;"></i>' +
|
|
'Don\'t Miss — Stage ' + (idx + 1) +
|
|
'</div>' +
|
|
stage.dontMiss.map(function(it) {
|
|
var why = it.why ? '<div style="font-size:11px;color:var(--g500);margin-top:1px;">' + escHtml(it.why) + '</div>' : '';
|
|
return '<div style="padding:4px 0;font-size:13px;color:var(--g800);">' +
|
|
'<i class="fas fa-circle-exclamation" style="color:#f59e0b;font-size:10px;margin-right:6px;"></i>' +
|
|
escHtml(it.point) +
|
|
why +
|
|
'</div>';
|
|
}).join('') +
|
|
'</div>';
|
|
}
|
|
card.innerHTML =
|
|
'<div class="card-header output-header">' +
|
|
'<h3>' +
|
|
'<i class="fas fa-file-medical"></i> Stage ' + (idx + 1) + ' Note' +
|
|
(modelShort ? ' <span class="model-tag" style="margin-left:6px;">' + escHtml(modelShort) + '</span>' : '') +
|
|
'</h3>' +
|
|
'<div class="output-actions">' +
|
|
'<button class="btn-sm btn-primary" data-action="copy" data-target="' + stageTextElId(idx) + '"><i class="fas fa-copy"></i> Copy</button>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'<div id="' + stageTextElId(idx) + '" class="output-text" contenteditable="' + editable + '"></div>' +
|
|
dontMissHtml;
|
|
// Set the note text via textContent so contenteditable shows the correct content
|
|
// (and so any HTML in the model output is treated as plain text, not parsed).
|
|
var textEl = card.querySelector('#' + stageTextElId(idx));
|
|
if (textEl) textEl.textContent = stage.note || '';
|
|
return card;
|
|
}
|
|
|
|
function renderFinalNote(note) {
|
|
var container = document.getElementById('ed-final-note-card');
|
|
if (!container) {
|
|
// Create the final-note card lazily and insert before the MDM card
|
|
container = document.createElement('div');
|
|
container.id = 'ed-final-note-card';
|
|
container.className = 'card output-card';
|
|
container.style.cssText = 'margin-top:10px;border-left:3px solid #2563eb;';
|
|
var mdm = document.getElementById('ed-mdm-card');
|
|
if (mdm && mdm.parentNode) mdm.parentNode.insertBefore(container, mdm);
|
|
else document.body.appendChild(container);
|
|
}
|
|
container.innerHTML =
|
|
'<div class="card-header output-header">' +
|
|
'<h3><i class="fas fa-file-medical-alt" style="color:#2563eb;"></i> Final Consolidated Note</h3>' +
|
|
'<div class="output-actions">' +
|
|
'<button class="btn-sm btn-primary" data-action="copy" data-target="ed-final-note-text"><i class="fas fa-copy"></i> Copy</button>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'<div id="ed-final-note-text" class="output-text"></div>';
|
|
var t = container.querySelector('#ed-final-note-text');
|
|
if (t) t.textContent = note;
|
|
if (typeof attachPatientEducation === 'function') attachPatientEducation('ed-final-note-text', { patientAge: getVal('ed-age') });
|
|
container.classList.remove('hidden');
|
|
container.scrollIntoView({ behavior: 'smooth' });
|
|
}
|
|
|
|
function renderMdm(mdm) {
|
|
var card = document.getElementById('ed-mdm-card');
|
|
var text = document.getElementById('ed-mdm-text');
|
|
var tag = document.getElementById('ed-mdm-level-tag');
|
|
if (!card || !text || !mdm) return;
|
|
if (tag) tag.textContent = mdm.suggestedLevel || '';
|
|
text.innerHTML =
|
|
'<div style="margin-bottom:6px;"><strong>Problems Addressed</strong> (' + escHtml(mdm.problemsAddressed) + ')<br>' + escHtml(mdm.problemsNarrative) + '</div>' +
|
|
'<div style="margin-bottom:6px;"><strong>Data Reviewed</strong> (' + escHtml(mdm.dataReviewed) + ')<br>' + escHtml(mdm.dataNarrative) + '</div>' +
|
|
'<div style="margin-bottom:6px;"><strong>Risk</strong> (' + escHtml(mdm.risk) + ')<br>' + escHtml(mdm.riskNarrative) + '</div>' +
|
|
'<div style="padding-top:8px;border-top:1px solid var(--g100);"><strong>Suggested Level: ' + escHtml(mdm.suggestedLevel) + '</strong><br>' +
|
|
'<span style="font-size:12px;color:var(--g600);">' + escHtml(mdm.levelRationale) + '</span></div>' +
|
|
'<p style="font-size:10px;color:var(--g400);margin:8px 0 0;">Suggestion only. Verify against your institution\'s coding guidelines.</p>';
|
|
card.classList.remove('hidden');
|
|
}
|
|
|
|
// Badge accurately reflects state.
|
|
// - "Stage N (recording)" — advanced past last generation; no note yet for stage N
|
|
// - "Stage N" — stage N's note has been generated
|
|
// - "Finalized" — Save & Done complete
|
|
function updateBadge() {
|
|
var badge = document.getElementById('ed-stage-badge');
|
|
var rec = document.getElementById('ed-rec-stage-num');
|
|
var gen = document.getElementById('ed-gen-stage-num');
|
|
if (rec) rec.textContent = String(_state.stage);
|
|
if (gen) gen.textContent = String(_state.stage);
|
|
if (!badge) return;
|
|
if (_state.finalized) {
|
|
badge.textContent = 'Finalized';
|
|
badge.style.background = '#d1fae5';
|
|
badge.style.color = '#047857';
|
|
return;
|
|
}
|
|
var generatedHere = _state.stages.length >= _state.stage;
|
|
if (generatedHere) {
|
|
badge.textContent = 'Stage ' + _state.stage;
|
|
badge.style.background = 'var(--g100)';
|
|
badge.style.color = 'var(--g600)';
|
|
} else {
|
|
badge.textContent = 'Stage ' + _state.stage + ' (recording)';
|
|
badge.style.background = '#fef3c7';
|
|
badge.style.color = '#92400e';
|
|
}
|
|
}
|
|
|
|
// ── Recording ────────────────────────────────────────────────────────
|
|
function initRecording() {
|
|
var recordBtn = document.getElementById('ed-record-btn');
|
|
var pauseBtn = document.getElementById('ed-pause-btn');
|
|
var indicator = document.getElementById('ed-rec-indicator');
|
|
var timerEl = document.getElementById('ed-timer');
|
|
var transcriptEl = document.getElementById('ed-transcript');
|
|
if (!recordBtn) return;
|
|
|
|
_timer = createTimer(timerEl);
|
|
_recognition = createSpeechRecognition();
|
|
if (_recognition) {
|
|
_recognition.onresult = function(e) {
|
|
var interim = '';
|
|
for (var i = e.resultIndex; i < e.results.length; i++) {
|
|
if (e.results[i].isFinal) _liveTranscript += e.results[i][0].transcript + ' ';
|
|
else interim = e.results[i][0].transcript;
|
|
}
|
|
if (transcriptEl) transcriptEl.innerHTML = escHtml(_liveTranscript) + (interim ? '<span style="color:#9ca3af;">' + escHtml(interim) + '</span>' : '');
|
|
};
|
|
_recognition.onend = function() { if (_recording && !_paused) try { _recognition.start(); } catch(e) {} };
|
|
}
|
|
|
|
recordBtn.addEventListener('click', function() {
|
|
if (!_recording) {
|
|
_liveTranscript = '';
|
|
_recorder = new AudioRecorder();
|
|
_recorder.start().then(function() {
|
|
_recording = true; _paused = false;
|
|
recordBtn.innerHTML = '<i class="fas fa-stop"></i> Stop';
|
|
recordBtn.classList.add('recording');
|
|
if (pauseBtn) pauseBtn.classList.remove('hidden');
|
|
indicator.classList.remove('hidden');
|
|
_timer.start();
|
|
if (_recognition) try { _recognition.start(); } catch(e) {}
|
|
showToast('ED recording started — Stage ' + _state.stage, 'info');
|
|
document.dispatchEvent(new CustomEvent('recording-started', { detail: { module: 'ed' } }));
|
|
}).catch(function() { showToast('Microphone denied', 'error'); });
|
|
} else {
|
|
_recording = false; _paused = false;
|
|
var dur = _timer.stop();
|
|
recordBtn.innerHTML = '<i class="fas fa-microphone"></i> Listen In';
|
|
recordBtn.classList.remove('recording');
|
|
if (pauseBtn) pauseBtn.classList.add('hidden');
|
|
indicator.classList.add('hidden');
|
|
if (_recognition) try { _recognition.stop(); } catch(e) {}
|
|
document.dispatchEvent(new CustomEvent('recording-stopped', { detail: { module: 'ed' } }));
|
|
|
|
showBusy('Transcribing...');
|
|
_recorder.stop().then(function(blob) {
|
|
if (!blob || blob.size === 0) {
|
|
hideBusy();
|
|
if (_liveTranscript.trim() && transcriptEl) transcriptEl.textContent = _liveTranscript.trim();
|
|
persistLocal();
|
|
return;
|
|
}
|
|
return transcribeAudio(blob, 'ed').then(function(data) {
|
|
hideBusy();
|
|
if (data && data.success && data.text) {
|
|
if (transcriptEl) transcriptEl.textContent = data.text;
|
|
showToast('Transcribed ' + dur + 's', 'success');
|
|
} else if (data && data.success) {
|
|
if (_liveTranscript.trim() && transcriptEl) transcriptEl.textContent = _liveTranscript.trim();
|
|
showToast('No speech detected in the recording', 'error');
|
|
} else if (_liveTranscript.trim() && transcriptEl) {
|
|
transcriptEl.textContent = _liveTranscript.trim();
|
|
}
|
|
persistLocal();
|
|
});
|
|
}).catch(function() { hideBusy(); });
|
|
}
|
|
});
|
|
|
|
if (pauseBtn) {
|
|
pauseBtn.addEventListener('click', function() {
|
|
if (!_recording || !_recorder || !_recorder.mediaRecorder) return;
|
|
if (!_paused) {
|
|
try { if (_recorder.mediaRecorder.state === 'recording') _recorder.mediaRecorder.pause(); } catch(e) {}
|
|
_timer.stop(); _paused = true;
|
|
pauseBtn.innerHTML = '<i class="fas fa-play"></i> Resume';
|
|
if (_recognition) try { _recognition.stop(); } catch(e) {}
|
|
} else {
|
|
_recorder.resumeCapture();
|
|
_timer.resume(); _paused = false;
|
|
pauseBtn.innerHTML = '<i class="fas fa-pause"></i> Pause';
|
|
if (_recognition) try { _recognition.start(); } catch(e) {}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// ── Generate (per-stage) ─────────────────────────────────────────────
|
|
function generateStage() {
|
|
if (!boundary.active()) return;
|
|
if (_state.finalized) { showToast('Encounter is finalized — start a new one', 'error'); return; }
|
|
var cc = getVal('ed-cc');
|
|
if (!cc.trim()) { showToast('Enter a chief complaint first', 'error'); return; }
|
|
var transcript = getText('ed-transcript');
|
|
if (!transcript) { showToast('No transcript — record or type the dictation first', 'error'); return; }
|
|
|
|
// Capture any in-place edits to earlier stages so the AI sees them as the
|
|
// baseline for the new stage.
|
|
gatherCurrentNotes();
|
|
|
|
var modelEl = document.getElementById('ed-model-select');
|
|
var selectedModel = modelEl ? modelEl.value : '';
|
|
|
|
showBusy('Generating ED note (Stage ' + _state.stage + ')...');
|
|
|
|
var memoriesPromise = (typeof getUserMemoryContext === 'function') ? getUserMemoryContext() : Promise.resolve('');
|
|
|
|
memoriesPromise.then(function(memCtx) {
|
|
var prevNote = _state.stages.length > 0 ? _state.stages[_state.stages.length - 1].note : '';
|
|
return fetch('/api/ed-encounters/generate', {
|
|
method: 'POST',
|
|
headers: getAuthHeaders(),
|
|
body: JSON.stringify({
|
|
stage: _state.stage,
|
|
transcript: transcript,
|
|
chiefComplaint: cc,
|
|
patientAge: getVal('ed-age'),
|
|
patientGender: getVal('ed-gender'),
|
|
previousNote: prevNote || undefined,
|
|
physicianMemories: memCtx || undefined,
|
|
model: selectedModel || undefined
|
|
})
|
|
});
|
|
})
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
if (!boundary.active()) return;
|
|
hideBusy();
|
|
if (!data.success) { showToast(data.error || 'Generation failed', 'error'); return; }
|
|
|
|
// Place this stage at index (_state.stage - 1). If the user is regenerating
|
|
// an existing stage (shouldn't happen via UI, but defensively), overwrite.
|
|
var idx = _state.stage - 1;
|
|
_state.stages[idx] = {
|
|
transcript: transcript,
|
|
note: data.note || '',
|
|
dontMiss: data.dontMiss || [],
|
|
model: data.model || '',
|
|
generatedAt: new Date().toISOString()
|
|
};
|
|
|
|
renderStages();
|
|
updateBadge();
|
|
persistLocal();
|
|
autoSaveDraft();
|
|
// Scroll to the newly added card
|
|
var card = document.querySelector('.stage-card[data-stage-idx="' + idx + '"]');
|
|
if (card) card.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
showToast('Stage ' + _state.stage + ' generated. Edit, add more, or Save & Done.', 'success');
|
|
})
|
|
.catch(function(err) { hideBusy(); showToast(err.message, 'error'); });
|
|
}
|
|
|
|
// ── Add another stage — DOES NOT change displayed cards ─────────────
|
|
function advanceStage() {
|
|
if (_state.finalized) { showToast('Encounter is finalized', 'error'); return; }
|
|
if (_state.stages.length < _state.stage) {
|
|
showToast('Generate the current stage before advancing', 'error');
|
|
return;
|
|
}
|
|
gatherCurrentNotes();
|
|
_state.stage += 1;
|
|
setText('ed-transcript', '');
|
|
_liveTranscript = '';
|
|
updateBadge();
|
|
persistLocal();
|
|
showToast('Ready for Stage ' + _state.stage + ' — record or type additional findings.', 'info');
|
|
var rec = document.getElementById('ed-record-btn');
|
|
if (rec) rec.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
}
|
|
|
|
// ── Finalize → consolidate + MDM + persist as final ──────────────────
|
|
function finalize() {
|
|
if (!boundary.active()) return;
|
|
if (_state.finalized) { showToast('Already finalized', 'info'); return; }
|
|
if (_state.stages.length === 0) { showToast('Generate at least one stage first', 'error'); return; }
|
|
var label = getVal('ed-label');
|
|
if (!label.trim()) { showToast('Enter a patient label before finalizing', 'error'); return; }
|
|
|
|
gatherCurrentNotes();
|
|
|
|
var modelEl = document.getElementById('ed-model-select');
|
|
var selectedModel = modelEl ? modelEl.value : '';
|
|
|
|
var encounterState = _state;
|
|
showBusy('Consolidating note + generating MDM...');
|
|
fetch('/api/ed-encounters/finalize', {
|
|
method: 'POST',
|
|
headers: getAuthHeaders(),
|
|
body: JSON.stringify({
|
|
stages: _state.stages.map(function(s) {
|
|
return { transcript: s.transcript || '', note: s.note || '' };
|
|
}),
|
|
chiefComplaint: getVal('ed-cc'),
|
|
patientAge: getVal('ed-age'),
|
|
patientGender: getVal('ed-gender'),
|
|
model: selectedModel || undefined
|
|
})
|
|
})
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
if (!boundary.active() || _state !== encounterState) return;
|
|
if (!data.success) { hideBusy(); showToast(data.error || 'Finalize failed', 'error'); return; }
|
|
_state.finalNote = data.finalNote || '';
|
|
_state.mdm = data.mdm;
|
|
_state.finalized = true;
|
|
renderStages(); // re-render with read-only stage cards
|
|
renderFinalNote(_state.finalNote);
|
|
renderMdm(_state.mdm);
|
|
updateBadge();
|
|
|
|
var fullTranscript = _state.stages.map(function(s, i) {
|
|
return '--- Stage ' + (i + 1) + ' ---\n' + (s.transcript || '');
|
|
}).join('\n\n');
|
|
var partial = { stages: _state.stages, finalNote: _state.finalNote, mdm: _state.mdm, finalized: true };
|
|
|
|
if (typeof saveEncounter === 'function') {
|
|
saveEncounter({
|
|
id: window._savedEncId_ed || null,
|
|
label: label,
|
|
enc_type: 'ed',
|
|
transcript: fullTranscript,
|
|
generated_note: composeFinalNoteForSave(_state.finalNote, _state.mdm),
|
|
partial_data: partial,
|
|
status: 'final',
|
|
onSaved: function(id) {
|
|
if (!boundary.active()) return;
|
|
window._savedEncId_ed = id;
|
|
try { sessionStorage.setItem(boundary.storageKey('_savedEncId_ed'), id); } catch(e) {}
|
|
clearLocal();
|
|
hideBusy();
|
|
showToast('Encounter finalized and saved.', 'success');
|
|
}
|
|
});
|
|
} else {
|
|
hideBusy();
|
|
}
|
|
})
|
|
.catch(function(err) {
|
|
if (!boundary.active() || _state !== encounterState) return;
|
|
hideBusy(); showToast(err.message, 'error');
|
|
});
|
|
}
|
|
|
|
function composeFinalNoteForSave(noteText, mdm) {
|
|
if (!mdm) return noteText;
|
|
return [
|
|
noteText,
|
|
'',
|
|
'Medical Decision Making (2023 E/M):',
|
|
'Problems Addressed (' + (mdm.problemsAddressed || '') + '): ' + (mdm.problemsNarrative || ''),
|
|
'Data Reviewed (' + (mdm.dataReviewed || '') + '): ' + (mdm.dataNarrative || ''),
|
|
'Risk (' + (mdm.risk || '') + '): ' + (mdm.riskNarrative || ''),
|
|
'Suggested Level: ' + (mdm.suggestedLevel || '') + ' — ' + (mdm.levelRationale || '')
|
|
].join('\n');
|
|
}
|
|
|
|
// ── Save draft (manual / auto on stage transition) ──────────────────
|
|
function autoSaveDraft() {
|
|
if (!boundary.active()) return;
|
|
var label = getVal('ed-label');
|
|
if (!label.trim()) return;
|
|
if (typeof saveEncounter !== 'function') return;
|
|
gatherCurrentNotes();
|
|
var fullTranscript = _state.stages.map(function(s) { return s.transcript || ''; }).join('\n\n');
|
|
var partial = { stages: _state.stages, finalized: false };
|
|
var lastNote = _state.stages.length ? _state.stages[_state.stages.length - 1].note : '';
|
|
saveEncounter({
|
|
id: window._savedEncId_ed || null,
|
|
label: label,
|
|
enc_type: 'ed',
|
|
transcript: fullTranscript,
|
|
generated_note: lastNote,
|
|
partial_data: partial,
|
|
status: 'draft',
|
|
onSaved: function(id) {
|
|
if (!boundary.active()) return;
|
|
window._savedEncId_ed = id;
|
|
try { sessionStorage.setItem(boundary.storageKey('_savedEncId_ed'), id); } catch(e) {}
|
|
}
|
|
});
|
|
}
|
|
|
|
function manualSaveDraft() {
|
|
if (!getVal('ed-label').trim()) { showToast('Enter a patient label first', 'error'); return; }
|
|
autoSaveDraft();
|
|
showToast('Draft saved', 'success');
|
|
}
|
|
|
|
// ── Reset / new patient ──────────────────────────────────────────────
|
|
function resetEncounter() {
|
|
_state = freshState();
|
|
hideBusy();
|
|
setVal('ed-label', '');
|
|
setVal('ed-age', '');
|
|
setVal('ed-gender', '');
|
|
setVal('ed-cc', '');
|
|
setText('ed-transcript', '');
|
|
_liveTranscript = '';
|
|
var container = document.getElementById('ed-stages-container');
|
|
if (container) container.innerHTML = '';
|
|
var finalCard = document.getElementById('ed-final-note-card');
|
|
if (finalCard) finalCard.remove();
|
|
hideEl('ed-tail-controls');
|
|
hideEl('ed-mdm-card');
|
|
window._savedEncId_ed = null;
|
|
try { sessionStorage.removeItem(boundary.storageKey('_savedEncId_ed')); } catch(e) {}
|
|
if (typeof window.resetIdempotencyKey === 'function') window.resetIdempotencyKey('ed');
|
|
clearLocal();
|
|
updateBadge();
|
|
showToast('ED encounter cleared for new patient', 'info');
|
|
}
|
|
|
|
// ── Refine the latest stage ──────────────────────────────────────────
|
|
function refineLatestStage() {
|
|
if (_state.finalized) { showToast('Encounter is finalized', 'error'); return; }
|
|
if (_state.stages.length === 0) { showToast('Generate a stage first', 'error'); return; }
|
|
var idx = _state.stages.length - 1;
|
|
var elId = stageTextElId(idx);
|
|
if (typeof refineDocument === 'function') refineDocument(elId, 'ed-refine-input');
|
|
}
|
|
|
|
function shortenLatestStage() {
|
|
if (_state.finalized) { showToast('Encounter is finalized', 'error'); return; }
|
|
if (_state.stages.length === 0) { showToast('Generate a stage first', 'error'); return; }
|
|
var idx = _state.stages.length - 1;
|
|
var elId = stageTextElId(idx);
|
|
if (typeof shortenDocument === 'function') shortenDocument(elId);
|
|
}
|
|
|
|
// ── Wire click handlers ──────────────────────────────────────────────
|
|
document.addEventListener('click', function(e) {
|
|
if (e.target.closest('#btn-ed-generate')) generateStage();
|
|
if (e.target.closest('#btn-ed-add-more')) advanceStage();
|
|
if (e.target.closest('#btn-ed-finalize')) finalize();
|
|
if (e.target.closest('#btn-ed-new')) resetEncounter();
|
|
if (e.target.closest('#btn-ed-save')) manualSaveDraft();
|
|
if (e.target.closest('#ed-refine-btn')) refineLatestStage();
|
|
if (e.target.closest('#ed-shorten-btn')) shortenLatestStage();
|
|
});
|
|
|
|
// Persist on input — capture stage edits via gatherCurrentNotes inside persistLocal
|
|
document.addEventListener('input', function(e) {
|
|
if (!e.target) return;
|
|
var t = e.target;
|
|
if (t.id === 'ed-label' || t.id === 'ed-age' || t.id === 'ed-gender' || t.id === 'ed-cc' || t.id === 'ed-transcript') {
|
|
persistLocal();
|
|
return;
|
|
}
|
|
if (t.id && t.id.indexOf('ed-stage-text-') === 0) {
|
|
persistLocal();
|
|
}
|
|
});
|
|
|
|
// ── Load handler (resume from saved encounters list) ────────────────
|
|
if (typeof registerEncounterLoadHandler === 'function') {
|
|
registerEncounterLoadHandler('ed', function(enc) {
|
|
_state = freshState();
|
|
setVal('ed-label', enc.label || '');
|
|
var partial = enc.partial_data;
|
|
try { if (typeof partial === 'string') partial = JSON.parse(partial); } catch (e) { partial = null; }
|
|
if (partial && partial.stages) {
|
|
_state.stages = partial.stages;
|
|
_state.stage = partial.stages.length || 1;
|
|
_state.finalized = !!partial.finalized;
|
|
_state.finalNote = partial.finalNote || null;
|
|
_state.mdm = partial.mdm || null;
|
|
}
|
|
renderStages();
|
|
if (_state.finalNote) renderFinalNote(_state.finalNote);
|
|
if (_state.mdm) renderMdm(_state.mdm);
|
|
if (enc.transcript && _state.stages.length === 0) setText('ed-transcript', enc.transcript);
|
|
updateBadge();
|
|
});
|
|
}
|
|
|
|
// ── Init on tab activation ───────────────────────────────────────────
|
|
var _inited = false;
|
|
document.addEventListener('tabChanged', function(e) {
|
|
if (e.detail.tab !== 'ed' || _inited) return;
|
|
_inited = true;
|
|
initRecording();
|
|
loadLocal();
|
|
updateBadge();
|
|
});
|
|
|
|
console.log('ED encounters module loaded');
|