Four changes batched: 1. ED Encounters tab (new) — multi-stage emergency note with don't-miss tooltips and 2023 E/M MDM finalize. New route /api/ed-encounters (generate per-stage + finalize MDM), new ed-encounters.js owning all client logic, new ed-encounter.html component, new template_ed memory category. Persists draft to localStorage every keystroke and to saved_encounters on stage advance. encounters.js touched only to register the new tab in sessionStorage restore + tabMap (save and idempotency code untouched). 2. Notes model selector — /notes/from-voice now accepts a client-supplied model (validated by the existing callAI allow-list); falls back to the admin default. Added <select class="tab-model-select"> to notes.html so the existing app.js populator handles options + default. 3. Remove AI-learning-from-corrections — deleted correctionTracker.js, POST /memories/correction, the corrections branch in /memories/context, the settings UI section, the FAQ entry, and all dead trackAIOutput/saveCorrection guards in callers. Legacy correction_* DB rows are filtered (NOT LIKE) rather than dropped, so no destructive migration. 4. Fix notes AI framing — /notes/from-voice prompt no longer assumes "physician dictation". Plain notes (shopping lists, reminders, ideas) now match the dictation tone instead of being forced into clinical structure. All 46 tests pass.
1061 lines
50 KiB
JavaScript
1061 lines
50 KiB
JavaScript
// ============================================================
|
|
// SHADESS.JS — SSHADESS psychosocial screening form + well visit note
|
|
// ============================================================
|
|
|
|
(function() {
|
|
|
|
// ── SSHADESS domain definitions ──────────────────────────────────────────
|
|
// Each domain has key questions (most important shown first) + comment field
|
|
var SHADESS_DOMAINS = [
|
|
{
|
|
key: 'strengths',
|
|
label: 'Strengths',
|
|
icon: 'fa-star',
|
|
color: '#f59e0b',
|
|
intro: 'Starting with what you do well helps us get to know you better.',
|
|
questions: [
|
|
{ id: 'str1', text: 'Has something they are proud of or enjoy', type: 'yn' },
|
|
{ id: 'str2', text: 'Describes self positively when asked', type: 'yn' },
|
|
{ id: 'str3', text: 'Has at least one trusted adult they can talk to', type: 'yn' },
|
|
]
|
|
},
|
|
{
|
|
key: 'school',
|
|
label: 'School',
|
|
icon: 'fa-graduation-cap',
|
|
color: '#3b82f6',
|
|
intro: 'Ask about school performance, attendance, and future plans.',
|
|
questions: [
|
|
{ id: 'sch1', text: 'Grades are satisfactory / doing their best', type: 'yn' },
|
|
{ id: 'sch2', text: 'Likes school or finds something enjoyable about it', type: 'yn' },
|
|
{ id: 'sch3', text: 'Regular attendance (no truancy concerns)', type: 'yn' },
|
|
{ id: 'sch4', text: 'Has plans or goals for the future', type: 'yn' },
|
|
]
|
|
},
|
|
{
|
|
key: 'home',
|
|
label: 'Home',
|
|
icon: 'fa-house',
|
|
color: '#10b981',
|
|
intro: 'Ask about living situation and family relationships.',
|
|
questions: [
|
|
{ id: 'hom1', text: 'Stable living situation', type: 'yn' },
|
|
{ id: 'hom2', text: 'Gets along with people at home', type: 'yn' },
|
|
{ id: 'hom3', text: 'Would talk to family member if stressed', type: 'yn' },
|
|
{ id: 'hom4', text: 'Has experienced household violence or instability (concern if YES)', type: 'yn', concern_if: true },
|
|
]
|
|
},
|
|
{
|
|
key: 'activities',
|
|
label: 'Activities',
|
|
icon: 'fa-users',
|
|
color: '#8b5cf6',
|
|
intro: 'Ask about friends, hobbies, and peer relationships.',
|
|
questions: [
|
|
{ id: 'act1', text: 'Has friends and spends time with them', type: 'yn' },
|
|
{ id: 'act2', text: 'Involved in sports, clubs, or hobbies', type: 'yn' },
|
|
{ id: 'act3', text: 'Social media/screen use within healthy limits', type: 'yn' },
|
|
{ id: 'act4', text: 'Has experienced bullying (concern if YES)', type: 'yn', concern_if: true },
|
|
]
|
|
},
|
|
{
|
|
key: 'drugs',
|
|
label: 'Drugs / Substances',
|
|
icon: 'fa-pills',
|
|
color: '#ef4444',
|
|
intro: 'This is confidential. Ask in private.',
|
|
questions: [
|
|
{ id: 'drg1', text: 'Has tried cigarettes / vaping / tobacco', type: 'yn', concern_if: true },
|
|
{ id: 'drg2', text: 'Has tried alcohol', type: 'yn', concern_if: true },
|
|
{ id: 'drg3', text: 'Has tried marijuana or other drugs', type: 'yn', concern_if: true },
|
|
{ id: 'drg4', text: 'Friends use substances', type: 'yn' },
|
|
{ id: 'drg5', text: 'CRAFFT screen result (if done)', type: 'text', placeholder: 'e.g., Score 0 — low risk' },
|
|
]
|
|
},
|
|
{
|
|
key: 'emotions',
|
|
label: 'Emotions / Eating',
|
|
icon: 'fa-heart',
|
|
color: '#ec4899',
|
|
intro: 'Screen for depression, anxiety, and disordered eating.',
|
|
questions: [
|
|
{ id: 'emo1', text: 'Feeling down, sad, or hopeless recently', type: 'yn', concern_if: true },
|
|
{ id: 'emo2', text: 'Feeling unusually stressed or anxious', type: 'yn', concern_if: true },
|
|
{ id: 'emo3', text: 'Trouble sleeping', type: 'yn' },
|
|
{ id: 'emo4', text: 'PHQ-A / depression screen result (if done)', type: 'text', placeholder: 'e.g., PHQ-A score 3 — minimal' },
|
|
{ id: 'emo5', text: 'Happy with eating habits and body image', type: 'yn' },
|
|
{ id: 'emo6', text: 'Restricting food / purging / using diet pills (concern if YES)', type: 'yn', concern_if: true },
|
|
]
|
|
},
|
|
{
|
|
key: 'sexuality',
|
|
label: 'Sexuality',
|
|
icon: 'fa-shield-heart',
|
|
color: '#f97316',
|
|
intro: 'Ask in private. Normalize the questions.',
|
|
questions: [
|
|
{ id: 'sex1', text: 'Comfortable discussing attraction/identity', type: 'yn' },
|
|
{ id: 'sex2', text: 'Sexually active', type: 'yn' },
|
|
{ id: 'sex3', text: 'Uses protection consistently if sexually active', type: 'yn' },
|
|
{ id: 'sex4', text: 'History of unwanted sexual contact (concern if YES)', type: 'yn', concern_if: true },
|
|
{ id: 'sex5', text: 'STI screening indicated/done', type: 'yn' },
|
|
]
|
|
},
|
|
{
|
|
key: 'safety',
|
|
label: 'Safety',
|
|
icon: 'fa-shield-halved',
|
|
color: '#6366f1',
|
|
intro: 'Ask about violence, weapons, and suicidal ideation.',
|
|
questions: [
|
|
{ id: 'saf1', text: 'Feels safe at school and home', type: 'yn' },
|
|
{ id: 'saf2', text: 'Carries a weapon (concern if YES)', type: 'yn', concern_if: true },
|
|
{ id: 'saf3', text: 'Has been in physical fights recently', type: 'yn', concern_if: true },
|
|
{ id: 'saf4', text: 'Wears seatbelt; safe driving practices', type: 'yn' },
|
|
{ id: 'saf5', text: 'Thoughts of hurting self or suicide (concern if YES — STAT eval)', type: 'yn', concern_if: true },
|
|
{ id: 'saf6', text: 'Columbia/ASQ suicide screen result (if done)', type: 'text', placeholder: 'e.g., ASQ: negative' },
|
|
]
|
|
}
|
|
];
|
|
|
|
// ── Render SHADESS form ──────────────────────────────────────────────────
|
|
var _shadessAnswers = {}; // key -> { questions: {id: answer}, comment: '', concern: false, skipped: false }
|
|
|
|
function initShadess() {
|
|
var container = document.getElementById('shadess-domains');
|
|
if (!container) return;
|
|
|
|
// Initialize answers object
|
|
SHADESS_DOMAINS.forEach(function(d) {
|
|
_shadessAnswers[d.key] = { questions: {}, comment: '', concern: false, skipped: false };
|
|
});
|
|
|
|
container.innerHTML = SHADESS_DOMAINS.map(function(domain) {
|
|
return renderDomain(domain);
|
|
}).join('');
|
|
|
|
// Wire up events
|
|
container.addEventListener('change', function(e) {
|
|
var el = e.target;
|
|
// yn question
|
|
if (el.classList.contains('shadess-yn')) {
|
|
var domainKey = el.dataset.domain;
|
|
var qid = el.dataset.qid;
|
|
if (_shadessAnswers[domainKey]) {
|
|
_shadessAnswers[domainKey].questions[qid] = el.value;
|
|
// Auto-flag concern
|
|
var domain = SHADESS_DOMAINS.find(function(d) { return d.key === domainKey; });
|
|
if (domain) {
|
|
var q = domain.questions.find(function(q) { return q.id === qid; });
|
|
if (q && q.concern_if === (el.value === 'yes')) {
|
|
_shadessAnswers[domainKey].concern = true;
|
|
var flagEl = document.getElementById('shadess-concern-' + domainKey);
|
|
if (flagEl) flagEl.classList.remove('hidden');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// text question
|
|
if (el.classList.contains('shadess-text')) {
|
|
if (_shadessAnswers[el.dataset.domain]) {
|
|
_shadessAnswers[el.dataset.domain].questions[el.dataset.qid] = el.value;
|
|
}
|
|
}
|
|
// comment
|
|
if (el.classList.contains('shadess-comment')) {
|
|
if (_shadessAnswers[el.dataset.domain]) {
|
|
_shadessAnswers[el.dataset.domain].comment = el.value;
|
|
}
|
|
}
|
|
// skip toggle
|
|
if (el.classList.contains('shadess-skip')) {
|
|
if (_shadessAnswers[el.dataset.domain]) {
|
|
_shadessAnswers[el.dataset.domain].skipped = el.checked;
|
|
var body = document.getElementById('shadess-body-' + el.dataset.domain);
|
|
if (body) body.style.opacity = el.checked ? '0.3' : '1';
|
|
}
|
|
}
|
|
});
|
|
|
|
// Concern flag manual toggle
|
|
container.addEventListener('click', function(e) {
|
|
if (e.target.closest('.shadess-flag-btn')) {
|
|
var btn = e.target.closest('.shadess-flag-btn');
|
|
var domainKey = btn.dataset.domain;
|
|
if (_shadessAnswers[domainKey]) {
|
|
_shadessAnswers[domainKey].concern = !_shadessAnswers[domainKey].concern;
|
|
var flagEl = document.getElementById('shadess-concern-' + domainKey);
|
|
if (flagEl) flagEl.classList.toggle('hidden', !_shadessAnswers[domainKey].concern);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function renderDomain(domain) {
|
|
var qHtml = domain.questions.map(function(q) {
|
|
if (q.type === 'yn') {
|
|
return '<div class="shadess-q-row">' +
|
|
'<span class="shadess-q-text">' + esc(q.text) + (q.concern_if !== undefined ? ' <span style="font-size:10px;color:var(--g400);">(flag if ' + (q.concern_if ? 'Yes' : 'No') + ')</span>' : '') + '</span>' +
|
|
'<div class="shadess-yn-btns">' +
|
|
'<select class="shadess-yn" data-domain="' + domain.key + '" data-qid="' + q.id + '" style="font-size:12px;padding:3px 6px;border:1px solid var(--g300);border-radius:6px;">' +
|
|
'<option value="">—</option><option value="yes">Yes</option><option value="no">No</option>' +
|
|
'</select>' +
|
|
'</div>' +
|
|
'</div>';
|
|
} else {
|
|
return '<div class="shadess-q-row">' +
|
|
'<span class="shadess-q-text">' + esc(q.text) + '</span>' +
|
|
'<input class="shadess-text" data-domain="' + domain.key + '" data-qid="' + q.id + '" type="text" placeholder="' + esc(q.placeholder || '') + '" style="font-size:12px;padding:4px 8px;border:1px solid var(--g300);border-radius:6px;flex:1;min-width:160px;">' +
|
|
'</div>';
|
|
}
|
|
}).join('');
|
|
|
|
return '<div class="shadess-domain" style="border-left:3px solid ' + domain.color + ';">' +
|
|
'<div class="shadess-domain-header">' +
|
|
'<i class="fas ' + domain.icon + '" style="color:' + domain.color + ';width:16px;"></i>' +
|
|
'<strong style="font-size:14px;">' + esc(domain.label) + '</strong>' +
|
|
'<span style="font-size:11px;color:var(--g500);flex:1;margin-left:6px;">' + esc(domain.intro) + '</span>' +
|
|
'<span id="shadess-concern-' + domain.key + '" class="shadess-concern-badge hidden"><i class="fas fa-triangle-exclamation"></i> Concern</span>' +
|
|
'<button class="shadess-flag-btn btn-sm btn-ghost" data-domain="' + domain.key + '" style="font-size:11px;" title="Toggle concern flag"><i class="fas fa-flag"></i></button>' +
|
|
'<label style="font-size:11px;color:var(--g500);display:flex;align-items:center;gap:4px;"><input type="checkbox" class="shadess-skip" data-domain="' + domain.key + '"> Skip</label>' +
|
|
'</div>' +
|
|
'<div id="shadess-body-' + domain.key + '" class="shadess-domain-body">' +
|
|
qHtml +
|
|
'<div class="shadess-q-row" style="align-items:flex-start;">' +
|
|
'<span class="shadess-q-text" style="padding-top:4px;">Additional notes:</span>' +
|
|
'<textarea class="shadess-comment" data-domain="' + domain.key + '" rows="2" style="flex:1;font-size:12px;padding:4px 8px;border:1px solid var(--g300);border-radius:6px;resize:vertical;" placeholder="Free text comments for this domain..."></textarea>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>';
|
|
}
|
|
|
|
// ── SHADESS recording (listen in) ────────────────────────────────────────
|
|
var _shadessRecorder = null;
|
|
var _shadessTimer = null;
|
|
var _shadessRecording = false;
|
|
var _shadessIsPaused = false;
|
|
var _shhadessRecognition = null;
|
|
var _shadessTranscript = '';
|
|
|
|
function initShadessRecording() {
|
|
var recordBtn = document.getElementById('shadess-record-btn');
|
|
var pauseBtn = document.getElementById('shadess-pause-btn');
|
|
var indicator = document.getElementById('shadess-rec-indicator');
|
|
var timerEl = document.getElementById('shadess-timer');
|
|
if (!recordBtn) return;
|
|
|
|
_shadessTimer = createTimer(timerEl);
|
|
_shhadessRecognition = createSpeechRecognition();
|
|
if (_shhadessRecognition) {
|
|
_shhadessRecognition.onresult = function(e) {
|
|
for (var i = e.resultIndex; i < e.results.length; i++) {
|
|
if (e.results[i].isFinal) _shadessTranscript += e.results[i][0].transcript + ' ';
|
|
}
|
|
};
|
|
_shhadessRecognition.onend = function() {
|
|
if (_shadessRecording && !_shadessIsPaused) try { _shhadessRecognition.start(); } catch(e) {}
|
|
};
|
|
}
|
|
|
|
recordBtn.addEventListener('click', function() {
|
|
if (!_shadessRecording) {
|
|
_shadessTranscript = '';
|
|
_shadessRecorder = new AudioRecorder();
|
|
_shadessRecorder.start().then(function() {
|
|
_shadessRecording = true; _shadessIsPaused = false;
|
|
recordBtn.innerHTML = '<i class="fas fa-stop"></i> Stop';
|
|
recordBtn.classList.add('recording');
|
|
if (pauseBtn) pauseBtn.classList.remove('hidden');
|
|
indicator.classList.remove('hidden');
|
|
_shadessTimer.start();
|
|
if (_shhadessRecognition) try { _shhadessRecognition.start(); } catch(e) {}
|
|
showToast('SSHADESS recording started', 'info');
|
|
}).catch(function() { showToast('Microphone denied', 'error'); });
|
|
} else {
|
|
_shadessRecording = false; _shadessIsPaused = false;
|
|
var dur = _shadessTimer.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 (_shhadessRecognition) try { _shhadessRecognition.stop(); } catch(e) {}
|
|
|
|
showBusy('Transcribing SSHADESS...');
|
|
_shadessRecorder.stop().then(function(blob) {
|
|
if (!blob || blob.size === 0) { hideBusy(); return; }
|
|
return transcribeAudio(blob).then(function(data) {
|
|
hideBusy();
|
|
if (data.success) {
|
|
_shadessTranscript = data.text;
|
|
showToast('SSHADESS audio transcribed (' + dur + 's)', 'success');
|
|
}
|
|
});
|
|
}).catch(function() { hideBusy(); });
|
|
}
|
|
});
|
|
|
|
if (pauseBtn) {
|
|
pauseBtn.addEventListener('click', function() {
|
|
if (!_shadessRecording || !_shadessRecorder || !_shadessRecorder.mediaRecorder) return;
|
|
if (!_shadessIsPaused) {
|
|
try { if (_shadessRecorder.mediaRecorder.state === 'recording') _shadessRecorder.mediaRecorder.pause(); } catch(e) {}
|
|
_shadessTimer.stop();
|
|
_shadessIsPaused = true;
|
|
pauseBtn.innerHTML = '<i class="fas fa-play"></i> Resume';
|
|
if (_shhadessRecognition) try { _shhadessRecognition.stop(); } catch(e) {}
|
|
} else {
|
|
try {
|
|
if (_shadessRecorder.mediaRecorder.state === 'paused') { _shadessRecorder.mediaRecorder.resume(); }
|
|
else if (_shadessRecorder.mediaRecorder.state === 'inactive' && _shadessRecorder.stream && _shadessRecorder.stream.active) {
|
|
var mime = MediaRecorder.isTypeSupported('audio/webm;codecs=opus') ? 'audio/webm;codecs=opus' : 'audio/webm';
|
|
_shadessRecorder.mediaRecorder = new MediaRecorder(_shadessRecorder.stream, { mimeType: mime, audioBitsPerSecond: 32000 });
|
|
_shadessRecorder.mediaRecorder.ondataavailable = function(e) { if (e.data.size > 0) _shadessRecorder.chunks.push(e.data); };
|
|
_shadessRecorder.mediaRecorder.start(1000);
|
|
}
|
|
} catch(e) {}
|
|
_shadessTimer.resume();
|
|
_shadessIsPaused = false;
|
|
pauseBtn.innerHTML = '<i class="fas fa-pause"></i> Pause';
|
|
if (_shhadessRecognition) try { _shhadessRecognition.start(); } catch(e) {}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// ── Generate SSHADESS assessment ─────────────────────────────────────────
|
|
function generateShadess() {
|
|
var age = document.getElementById('shadess-age').value;
|
|
var gender = document.getElementById('shadess-gender').value;
|
|
|
|
// Build domains payload from answers
|
|
var domains = {};
|
|
SHADESS_DOMAINS.forEach(function(d) {
|
|
var ans = _shadessAnswers[d.key];
|
|
var questions = d.questions.map(function(q) {
|
|
return { id: q.id, text: q.text, answer: ans.questions[q.id] || null };
|
|
}).filter(function(q) { return q.answer !== null && q.answer !== ''; });
|
|
|
|
domains[d.key] = {
|
|
skipped: ans.skipped,
|
|
questions: questions,
|
|
comment: ans.comment,
|
|
concern: ans.concern
|
|
};
|
|
});
|
|
|
|
// Check something is filled in
|
|
var hasData = Object.values(domains).some(function(d) {
|
|
return !d.skipped && (d.questions.length > 0 || d.comment);
|
|
});
|
|
if (!hasData && !_shadessTranscript) {
|
|
showToast('Fill in at least one domain before generating', 'error');
|
|
return;
|
|
}
|
|
|
|
showBusy('Generating SSHADESS assessment...');
|
|
|
|
var modelEl = document.querySelector('#wv-panel-shadess .tab-model-select');
|
|
|
|
fetch('/api/well-visit/shadess', {
|
|
method: 'POST',
|
|
headers: getAuthHeaders(),
|
|
body: JSON.stringify({
|
|
patientAge: age,
|
|
patientGender: gender,
|
|
domains: domains,
|
|
dictationText: _shadessTranscript || null,
|
|
model: modelEl ? modelEl.value : getSelectedModel()
|
|
})
|
|
})
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
hideBusy();
|
|
if (data.success) {
|
|
var out = document.getElementById('shadess-result-text');
|
|
var outCard = document.getElementById('shadess-output');
|
|
var tag = document.getElementById('shadess-model-tag');
|
|
if (out) setOutputText(out, data.assessment);
|
|
if (tag) tag.textContent = (data.model || '').split('/').pop();
|
|
if (outCard) { outCard.classList.remove('hidden'); outCard.scrollIntoView({ behavior: 'smooth' }); }
|
|
showToast('SSHADESS assessment generated', 'success');
|
|
// Auto-fill well visit note SSHADESS field
|
|
var wvShadess = document.getElementById('wv-shadess-text');
|
|
if (wvShadess) wvShadess.value = data.assessment;
|
|
} else {
|
|
showToast(data.error || 'Generation failed', 'error');
|
|
}
|
|
})
|
|
.catch(function(err) { hideBusy(); showToast(err.message, 'error'); });
|
|
}
|
|
|
|
// ── Well Visit Note — ROS data ────────────────────────────────────────────
|
|
var _rosData = {}; // { constitutionalStatus: 'wnl'|'abnormal'|'', constitutionalNote: '...', ... }
|
|
var _peData = {};
|
|
|
|
// 15 ROS systems
|
|
var ROS_SYSTEMS = [
|
|
{ key: 'constitutional', label: 'Constitutional', detail: 'fever, fatigue, weight loss/gain, appetite' },
|
|
{ key: 'eyes', label: 'Eyes', detail: 'vision changes, discharge, redness' },
|
|
{ key: 'ent', label: 'ENT', detail: 'ear pain, congestion, sore throat, mouth sores' },
|
|
{ key: 'cardiovascular', label: 'Cardiovascular', detail: 'chest pain, palpitations, murmur' },
|
|
{ key: 'respiratory', label: 'Respiratory', detail: 'cough, wheeze, dyspnea' },
|
|
{ key: 'gastrointestinal', label: 'Gastrointestinal', detail: 'nausea, vomiting, diarrhea, constipation, abdominal pain' },
|
|
{ key: 'genitourinary', label: 'Genitourinary', detail: 'dysuria, frequency, discharge, menstrual' },
|
|
{ key: 'musculoskeletal', label: 'Musculoskeletal', detail: 'joint pain, swelling, gait issues, back pain' },
|
|
{ key: 'skin', label: 'Skin', detail: 'rash, lesions, itch, hair/nail changes' },
|
|
{ key: 'neurological', label: 'Neurological', detail: 'headache, dizziness, seizures, numbness' },
|
|
{ key: 'psychiatric', label: 'Psychiatric', detail: 'mood, anxiety, sleep, behavior changes' },
|
|
{ key: 'endocrine', label: 'Endocrine', detail: 'polyuria/polydipsia, heat/cold intolerance, growth concerns' },
|
|
{ key: 'hematologic', label: 'Hematologic/Lymphatic', detail: 'bruising, bleeding, lymph nodes' },
|
|
{ key: 'allergic', label: 'Allergic/Immunologic', detail: 'allergic reactions, frequent infections' },
|
|
{ key: 'developmental', label: 'Developmental/Behavioral', detail: 'milestones, school, social' },
|
|
];
|
|
|
|
// 17 PE systems
|
|
var PE_SYSTEMS = [
|
|
{ key: 'general', label: 'General Appearance', detail: 'WDWN, alert, NAD, etc.' },
|
|
{ key: 'peEyes', label: 'Eyes', detail: 'PERRL, conjunctiva, EOM' },
|
|
{ key: 'ears', label: 'Ears', detail: 'TMs, canals, hearing' },
|
|
{ key: 'nose', label: 'Nose', detail: 'mucosa, septum, turbinates' },
|
|
{ key: 'mouthThroat', label: 'Mouth/Throat', detail: 'lips, gums, teeth, tonsils, pharynx' },
|
|
{ key: 'neck', label: 'Neck', detail: 'lymph nodes, thyroid, masses' },
|
|
{ key: 'chestLungs', label: 'Chest/Lungs', detail: 'air entry, wheezing, retractions' },
|
|
{ key: 'peCv', label: 'Cardiovascular', detail: 'S1/S2, murmur, pulses, cap refill' },
|
|
{ key: 'abdomen', label: 'Abdomen', detail: 'soft, tenderness, organomegaly, BS' },
|
|
{ key: 'guTanner', label: 'Genitourinary/Tanner', detail: 'Tanner stage, genitalia, hernias' },
|
|
{ key: 'msk', label: 'Musculoskeletal', detail: 'strength, ROM, gait, spine' },
|
|
{ key: 'extremities', label: 'Extremities', detail: 'tone, reflexes, clubbing, edema' },
|
|
{ key: 'peSkin', label: 'Skin', detail: 'rashes, lesions, cafe au lait, bruises' },
|
|
{ key: 'peNeuro', label: 'Neurological', detail: 'CN intact, DTRs, coordination' },
|
|
{ key: 'lymphatic', label: 'Lymphatic', detail: 'cervical, axillary, inguinal nodes' },
|
|
{ key: 'breast', label: 'Breast', detail: 'if indicated by age/Tanner' },
|
|
{ key: 'pePsych', label: 'Psychiatric', detail: 'affect, mood, behavior during exam' },
|
|
];
|
|
|
|
// ── Shared ROS row renderer (used by well visit and sick visit) ──────────
|
|
// dataObj = reference to _rosData or _peData
|
|
// systemsList = ROS_SYSTEMS or PE_SYSTEMS
|
|
// prefix = 'ros' | 'pe'
|
|
// btnLabels = { wnl:'WNL', abnormal:'Abnormal', notrev:'Not reviewed' } or { wnl:'Normal', abnormal:'Abnormal', notrev:'Not examined' }
|
|
window.renderRosRows = function(systemsList, dataObj, prefix, btnLabels) {
|
|
var bl = btnLabels || { wnl: 'WNL', abnormal: 'Abnormal', notrev: 'Not reviewed' };
|
|
return systemsList.map(function(sys) {
|
|
var status = dataObj[sys.key + '_status'] || '';
|
|
var note = dataObj[sys.key + '_note'] || '';
|
|
var wnlAct = status === 'wnl' ? ' wnl' : '';
|
|
var abnAct = status === 'abnormal' ? ' abnormal' : '';
|
|
var notrevAct = status === 'notrev' ? ' notrev' : '';
|
|
var noteVis = status === 'abnormal' ? ' visible' : '';
|
|
return '<div class="ros-system-row" data-sys-key="' + esc(sys.key) + '" data-prefix="' + prefix + '">' +
|
|
'<span class="ros-system-name" title="' + esc(sys.detail) + '">' + esc(sys.label) + ' <span style="font-size:10px;color:var(--g400);">(' + esc(sys.detail) + ')</span></span>' +
|
|
'<button class="ros-status-btn' + wnlAct + '" data-status="wnl" data-sys="' + esc(sys.key) + '" data-prefix="' + prefix + '">' + esc(bl.wnl) + '</button>' +
|
|
'<button class="ros-status-btn' + abnAct + '" data-status="abnormal" data-sys="' + esc(sys.key) + '" data-prefix="' + prefix + '">' + esc(bl.abnormal) + '</button>' +
|
|
'<button class="ros-status-btn' + notrevAct + '" data-status="notrev" data-sys="' + esc(sys.key) + '" data-prefix="' + prefix + '">' + esc(bl.notrev) + '</button>' +
|
|
'<input class="ros-note-input' + noteVis + '" data-sys="' + esc(sys.key) + '" data-prefix="' + prefix + '" type="text" placeholder="Describe..." value="' + esc(note) + '">' +
|
|
'</div>';
|
|
}).join('');
|
|
};
|
|
|
|
// Wire ROS/PE click events on a container element (idempotent — safe to call multiple times)
|
|
window.wireRosContainer = function(containerEl, dataObj) {
|
|
// Remove previous handler to prevent duplicate listeners on re-render
|
|
if (containerEl._rosClickHandler) containerEl.removeEventListener('click', containerEl._rosClickHandler);
|
|
if (containerEl._rosInputHandler) containerEl.removeEventListener('input', containerEl._rosInputHandler);
|
|
|
|
containerEl._rosClickHandler = function(e) {
|
|
var btn = e.target.closest('.ros-status-btn');
|
|
if (!btn) return;
|
|
var sysKey = btn.dataset.sys;
|
|
var status = btn.dataset.status;
|
|
var prefix = btn.dataset.prefix;
|
|
if (!sysKey) return;
|
|
|
|
// Toggle
|
|
var cur = dataObj[sysKey + '_status'] || '';
|
|
dataObj[sysKey + '_status'] = (cur === status) ? '' : status;
|
|
|
|
// Update buttons in row
|
|
var row = btn.closest('.ros-system-row');
|
|
if (row) {
|
|
row.querySelectorAll('.ros-status-btn').forEach(function(b) {
|
|
b.classList.remove('wnl', 'abnormal', 'notrev');
|
|
if (dataObj[sysKey + '_status'] && b.dataset.status === dataObj[sysKey + '_status']) {
|
|
b.classList.add(dataObj[sysKey + '_status']);
|
|
}
|
|
});
|
|
var noteInput = row.querySelector('.ros-note-input');
|
|
if (noteInput) {
|
|
if (dataObj[sysKey + '_status'] === 'abnormal') {
|
|
noteInput.classList.add('visible');
|
|
noteInput.focus();
|
|
} else {
|
|
noteInput.classList.remove('visible');
|
|
}
|
|
}
|
|
}
|
|
};
|
|
containerEl.addEventListener('click', containerEl._rosClickHandler);
|
|
|
|
containerEl._rosInputHandler = function(e) {
|
|
var inp = e.target.closest('.ros-note-input');
|
|
if (!inp) return;
|
|
var sysKey = inp.dataset.sys;
|
|
if (sysKey) dataObj[sysKey + '_note'] = inp.value;
|
|
};
|
|
containerEl.addEventListener('input', containerEl._rosInputHandler);
|
|
};
|
|
|
|
// Format ROS data as text for AI
|
|
window.formatRosForAI = function(systemsList, dataObj, heading) {
|
|
var lines = [heading + ':'];
|
|
var hasAny = false;
|
|
systemsList.forEach(function(sys) {
|
|
var status = dataObj[sys.key + '_status'] || '';
|
|
if (!status) return;
|
|
hasAny = true;
|
|
var note = dataObj[sys.key + '_note'] || '';
|
|
if (status === 'wnl') {
|
|
// WNL — tell AI to expand with pertinent negatives, include domain details for context
|
|
lines.push(' ' + sys.label + ': NORMAL [domain: ' + sys.detail + ']');
|
|
} else if (status === 'abnormal') {
|
|
// Abnormal — user's note is the finding; AI should expand/describe it
|
|
lines.push(' ' + sys.label + ': ABNORMAL' + (note ? ' — ' + note : '') + ' [domain: ' + sys.detail + ']');
|
|
} else {
|
|
lines.push(' ' + sys.label + ': Not reviewed');
|
|
}
|
|
});
|
|
return hasAny ? lines.join('\n') : '';
|
|
};
|
|
|
|
// ── ICD-10 Diagnoses ──────────────────────────────────────────────────────
|
|
var COMMON_DX = [
|
|
{ code: 'Z00.129', name: 'Routine child health exam, no abnormal findings' },
|
|
{ code: 'Z00.121', name: 'Routine child health exam, with abnormal findings' },
|
|
{ code: 'J06.9', name: 'Upper Respiratory Infection, NOS' },
|
|
{ code: 'J02.9', name: 'Acute pharyngitis, NOS' },
|
|
{ code: 'J03.90', name: 'Acute tonsillitis, NOS' },
|
|
{ code: 'H66.90', name: 'Otitis media, NOS' },
|
|
{ code: 'J20.9', name: 'Acute bronchitis' },
|
|
{ code: 'J45.20', name: 'Mild intermittent asthma, uncomplicated' },
|
|
{ code: 'J45.30', name: 'Mild persistent asthma' },
|
|
{ code: 'A09', name: 'Gastroenteritis/colitis' },
|
|
{ code: 'L20.9', name: 'Atopic dermatitis, NOS' },
|
|
{ code: 'L30.9', name: 'Dermatitis NOS' },
|
|
{ code: 'R50.9', name: 'Fever NOS' },
|
|
{ code: 'R10.9', name: 'Abdominal pain NOS' },
|
|
{ code: 'R05.9', name: 'Cough' },
|
|
{ code: 'B34.9', name: 'Viral infection NOS' },
|
|
{ code: 'Z23', name: 'Immunization encounter' },
|
|
{ code: 'Z41.9', name: 'Procedure for non-remedial reason' },
|
|
{ code: 'K21.0', name: 'GERD with esophagitis' },
|
|
{ code: 'K21.9', name: 'GERD without esophagitis' },
|
|
{ code: 'F90.0', name: 'ADHD, predominantly inattentive' },
|
|
{ code: 'F90.2', name: 'ADHD, combined type' },
|
|
{ code: 'F41.1', name: 'Generalized anxiety disorder' },
|
|
{ code: 'F32.9', name: 'Major depressive episode NOS' },
|
|
{ code: 'E11.9', name: 'Type 2 diabetes' },
|
|
{ code: 'E03.9', name: 'Hypothyroidism NOS' },
|
|
{ code: 'M54.5', name: 'Low back pain' },
|
|
{ code: 'G43.909', name: 'Migraine NOS' },
|
|
];
|
|
|
|
// 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 resultsId = idPrefix + '-dx-results';
|
|
var chipsId = idPrefix + '-dx-chips';
|
|
|
|
containerEl.innerHTML =
|
|
'<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;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>';
|
|
|
|
// Render quick-access chips
|
|
var chipsEl = containerEl.querySelector('#' + chipsId);
|
|
if (chipsEl) {
|
|
chipsEl.innerHTML = COMMON_DX.map(function(dx) {
|
|
return '<span class="dx-chip" data-code="' + esc(dx.code) + '" data-name="' + esc(dx.name) + '">' + esc(dx.code) + ' ' + esc(dx.name) + '</span>';
|
|
}).join('');
|
|
chipsEl.addEventListener('click', function(e) {
|
|
var chip = e.target.closest('.dx-chip');
|
|
if (!chip) return;
|
|
addDx(selectedDxArr, { code: chip.dataset.code, name: chip.dataset.name }, containerEl, tagsId);
|
|
});
|
|
}
|
|
|
|
// Live search
|
|
var searchEl = containerEl.querySelector('#' + searchId);
|
|
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 (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 === '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);
|
|
});
|
|
}
|
|
|
|
renderDxTags(selectedDxArr, containerEl, tagsId);
|
|
};
|
|
|
|
function addDx(arr, dx, containerEl, tagsId) {
|
|
// Avoid duplicates
|
|
var alreadyExists = arr.some(function(d) {
|
|
return d.code === dx.code && d.name === dx.name;
|
|
});
|
|
if (alreadyExists) return;
|
|
arr.push(dx);
|
|
renderDxTags(arr, containerEl, tagsId);
|
|
}
|
|
|
|
function renderDxTags(arr, containerEl, tagsId) {
|
|
var tagsEl = containerEl.querySelector('#' + tagsId);
|
|
if (!tagsEl) return;
|
|
tagsEl.innerHTML = arr.map(function(dx, i) {
|
|
return '<span class="dx-tag">' +
|
|
(dx.code ? '<strong>' + esc(dx.code) + '</strong> ' : '') + esc(dx.name) +
|
|
'<span class="dx-remove" data-idx="' + i + '" title="Remove">✕</span>' +
|
|
'</span>';
|
|
}).join('');
|
|
tagsEl.querySelectorAll('.dx-remove').forEach(function(btn) {
|
|
btn.addEventListener('click', function() {
|
|
var idx = parseInt(btn.dataset.idx);
|
|
arr.splice(idx, 1);
|
|
renderDxTags(arr, containerEl, tagsId);
|
|
});
|
|
});
|
|
}
|
|
|
|
window.formatDxForAI = function(selectedDxArr, freetextId) {
|
|
var lines = [];
|
|
selectedDxArr.forEach(function(dx) {
|
|
lines.push((dx.code ? dx.code + ' ' : '') + dx.name);
|
|
});
|
|
var freetextEl = freetextId ? document.getElementById(freetextId) : null;
|
|
if (freetextEl && freetextEl.value.trim()) {
|
|
lines.push(freetextEl.value.trim());
|
|
}
|
|
return lines.length ? 'Diagnoses:\n' + lines.map(function(l) { return ' - ' + l; }).join('\n') : '';
|
|
};
|
|
|
|
// ── Well Visit Note — module state ─────────────────────────────────────────
|
|
var _wvSelectedDx = [];
|
|
|
|
// ── Well Visit Note Generation ───────────────────────────────────────────
|
|
var _wvRecorder = null;
|
|
var _wvTimer = null;
|
|
var _wvRecording = false;
|
|
var _wvPaused = false;
|
|
var _wvRecognition = null;
|
|
var _wvTranscript = '';
|
|
|
|
function initWvRecording() {
|
|
var recordBtn = document.getElementById('wv-record-btn');
|
|
var pauseBtn = document.getElementById('wv-pause-btn');
|
|
var indicator = document.getElementById('wv-rec-indicator');
|
|
var timerEl = document.getElementById('wv-timer');
|
|
var transcriptEl = document.getElementById('wv-transcript');
|
|
if (!recordBtn) return;
|
|
|
|
_wvTimer = createTimer(timerEl);
|
|
_wvRecognition = createSpeechRecognition();
|
|
if (_wvRecognition) {
|
|
_wvRecognition.onresult = function(e) {
|
|
var interim = '';
|
|
for (var i = e.resultIndex; i < e.results.length; i++) {
|
|
if (e.results[i].isFinal) _wvTranscript += e.results[i][0].transcript + ' ';
|
|
else interim = e.results[i][0].transcript;
|
|
}
|
|
if (transcriptEl) transcriptEl.innerHTML = _wvTranscript + (interim ? '<span style="color:#9ca3af;">' + interim + '</span>' : '');
|
|
};
|
|
_wvRecognition.onend = function() { if (_wvRecording && !_wvPaused) try { _wvRecognition.start(); } catch(e) {} };
|
|
}
|
|
|
|
recordBtn.addEventListener('click', function() {
|
|
if (!_wvRecording) {
|
|
_wvTranscript = '';
|
|
_wvRecorder = new AudioRecorder();
|
|
_wvRecorder.start().then(function() {
|
|
_wvRecording = true; _wvPaused = false;
|
|
recordBtn.innerHTML = '<i class="fas fa-stop"></i> Stop';
|
|
recordBtn.classList.add('recording');
|
|
if (pauseBtn) pauseBtn.classList.remove('hidden');
|
|
indicator.classList.remove('hidden');
|
|
_wvTimer.start();
|
|
if (_wvRecognition) try { _wvRecognition.start(); } catch(e) {}
|
|
}).catch(function() { showToast('Microphone denied', 'error'); });
|
|
} else {
|
|
_wvRecording = false; _wvPaused = false;
|
|
var dur = _wvTimer.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 (_wvRecognition) try { _wvRecognition.stop(); } catch(e) {}
|
|
|
|
showBusy('Transcribing...');
|
|
_wvRecorder.stop().then(function(blob) {
|
|
if (!blob || blob.size === 0) { hideBusy(); if (_wvTranscript.trim() && transcriptEl) transcriptEl.textContent = _wvTranscript.trim(); return; }
|
|
return transcribeAudio(blob).then(function(data) {
|
|
hideBusy();
|
|
if (data.success) { if (transcriptEl) transcriptEl.textContent = data.text; _wvTranscript = data.text; showToast('Transcribed ' + dur + 's', 'success'); }
|
|
else { if (_wvTranscript.trim() && transcriptEl) transcriptEl.textContent = _wvTranscript.trim(); }
|
|
});
|
|
}).catch(function() { hideBusy(); });
|
|
}
|
|
});
|
|
|
|
if (pauseBtn) {
|
|
pauseBtn.addEventListener('click', function() {
|
|
if (!_wvRecording || !_wvRecorder || !_wvRecorder.mediaRecorder) return;
|
|
if (!_wvPaused) {
|
|
try { if (_wvRecorder.mediaRecorder.state === 'recording') _wvRecorder.mediaRecorder.pause(); } catch(e) {}
|
|
_wvTimer.stop(); _wvPaused = true;
|
|
pauseBtn.innerHTML = '<i class="fas fa-play"></i> Resume';
|
|
if (_wvRecognition) try { _wvRecognition.stop(); } catch(e) {}
|
|
} else {
|
|
try {
|
|
if (_wvRecorder.mediaRecorder.state === 'paused') { _wvRecorder.mediaRecorder.resume(); }
|
|
else if (_wvRecorder.mediaRecorder.state === 'inactive' && _wvRecorder.stream && _wvRecorder.stream.active) {
|
|
var mime = MediaRecorder.isTypeSupported('audio/webm;codecs=opus') ? 'audio/webm;codecs=opus' : 'audio/webm';
|
|
_wvRecorder.mediaRecorder = new MediaRecorder(_wvRecorder.stream, { mimeType: mime, audioBitsPerSecond: 32000 });
|
|
_wvRecorder.mediaRecorder.ondataavailable = function(e) { if (e.data.size > 0) _wvRecorder.chunks.push(e.data); };
|
|
_wvRecorder.mediaRecorder.start(1000);
|
|
}
|
|
} catch(e) {}
|
|
_wvTimer.resume(); _wvPaused = false;
|
|
pauseBtn.innerHTML = '<i class="fas fa-pause"></i> Pause';
|
|
if (_wvRecognition) try { _wvRecognition.start(); } catch(e) {}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// ── ROS/PE init for well visit ────────────────────────────────────────────
|
|
function initWvRosPe() {
|
|
var rosContainer = document.getElementById('wv-ros-container');
|
|
var peContainer = document.getElementById('wv-pe-container');
|
|
var dxContainer = document.getElementById('wv-dx-container');
|
|
var allWnlBtn = document.getElementById('wv-ros-all-wnl');
|
|
var allNormalBtn = document.getElementById('wv-pe-all-normal');
|
|
|
|
if (rosContainer) {
|
|
rosContainer.innerHTML = window.renderRosRows(ROS_SYSTEMS, _rosData, 'ros', { wnl: 'WNL', abnormal: 'Abnormal', notrev: 'Not reviewed' });
|
|
window.wireRosContainer(rosContainer, _rosData);
|
|
}
|
|
|
|
if (peContainer) {
|
|
peContainer.innerHTML = window.renderRosRows(PE_SYSTEMS, _peData, 'pe', { wnl: 'Normal', abnormal: 'Abnormal', notrev: 'Not examined' });
|
|
window.wireRosContainer(peContainer, _peData);
|
|
}
|
|
|
|
function refreshRos() {
|
|
if (rosContainer) {
|
|
rosContainer.innerHTML = window.renderRosRows(ROS_SYSTEMS, _rosData, 'ros', { wnl: 'WNL', abnormal: 'Abnormal', notrev: 'Not reviewed' });
|
|
window.wireRosContainer(rosContainer, _rosData);
|
|
}
|
|
}
|
|
function refreshPe() {
|
|
if (peContainer) {
|
|
peContainer.innerHTML = window.renderRosRows(PE_SYSTEMS, _peData, 'pe', { wnl: 'Normal', abnormal: 'Abnormal', notrev: 'Not examined' });
|
|
window.wireRosContainer(peContainer, _peData);
|
|
}
|
|
}
|
|
|
|
if (allWnlBtn) {
|
|
allWnlBtn.onclick = function() {
|
|
ROS_SYSTEMS.forEach(function(sys) { _rosData[sys.key + '_status'] = 'wnl'; });
|
|
refreshRos();
|
|
showToast('All ROS set to WNL', 'success');
|
|
};
|
|
}
|
|
|
|
var rosClearBtn = document.getElementById('wv-ros-clear');
|
|
if (rosClearBtn) {
|
|
rosClearBtn.onclick = function() {
|
|
ROS_SYSTEMS.forEach(function(sys) { _rosData[sys.key + '_status'] = ''; _rosData[sys.key + '_note'] = ''; });
|
|
refreshRos();
|
|
showToast('ROS cleared', 'info');
|
|
};
|
|
}
|
|
|
|
if (allNormalBtn) {
|
|
allNormalBtn.onclick = function() {
|
|
PE_SYSTEMS.forEach(function(sys) { _peData[sys.key + '_status'] = 'wnl'; });
|
|
refreshPe();
|
|
showToast('All PE set to Normal', 'success');
|
|
};
|
|
}
|
|
|
|
var peClearBtn = document.getElementById('wv-pe-clear');
|
|
if (peClearBtn) {
|
|
peClearBtn.onclick = function() {
|
|
PE_SYSTEMS.forEach(function(sys) { _peData[sys.key + '_status'] = ''; _peData[sys.key + '_note'] = ''; });
|
|
refreshPe();
|
|
showToast('PE cleared', 'info');
|
|
};
|
|
}
|
|
|
|
if (dxContainer) {
|
|
window.renderDxComponent(dxContainer, _wvSelectedDx, 'wv');
|
|
}
|
|
}
|
|
|
|
function generateWvNote() {
|
|
var age = document.getElementById('wv-note-age').value;
|
|
var gender = document.getElementById('wv-note-gender').value;
|
|
if (!age) { showToast('Enter patient age/visit age', 'error'); return; }
|
|
|
|
var transcriptEl = document.getElementById('wv-transcript');
|
|
var transcript = transcriptEl ? (transcriptEl.innerText || transcriptEl.textContent || '') : '';
|
|
|
|
showBusy('Generating well visit note...');
|
|
|
|
var modelEl = document.querySelector('#wv-panel-note .tab-model-select');
|
|
|
|
// Format ROS, PE, DX
|
|
var rosText = window.formatRosForAI(ROS_SYSTEMS, _rosData, 'Review of Systems');
|
|
var peText = window.formatRosForAI(PE_SYSTEMS, _peData, 'Physical Examination');
|
|
var dxText = window.formatDxForAI(_wvSelectedDx, 'wv-dx-freetext');
|
|
|
|
// Fetch user memories to include physician templates
|
|
var memoriesPromise = (typeof getUserMemoryContext === 'function') ? getUserMemoryContext() : Promise.resolve('');
|
|
|
|
memoriesPromise.then(function(memCtx) {
|
|
return fetch('/api/well-visit/note', {
|
|
method: 'POST',
|
|
headers: getAuthHeaders(),
|
|
body: JSON.stringify({
|
|
patientAge: age,
|
|
visitAge: age,
|
|
patientGender: gender,
|
|
vitals: document.getElementById('wv-vitals').value,
|
|
transcript: transcript || null,
|
|
screenings: document.getElementById('wv-screenings').value,
|
|
vaccines: document.getElementById('wv-vaccines').value,
|
|
shadessAssessment: document.getElementById('wv-shadess-text').value || null,
|
|
ros: rosText || null,
|
|
physicalExam: peText || null,
|
|
diagnoses: dxText || null,
|
|
physicianMemories: memCtx || null,
|
|
noteStyle: document.getElementById('wv-note-style').value,
|
|
model: modelEl ? modelEl.value : getSelectedModel()
|
|
})
|
|
});
|
|
})
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
hideBusy();
|
|
if (data.success) {
|
|
var noteEl = document.getElementById('wv-note-text');
|
|
var outCard = document.getElementById('wv-note-output');
|
|
var tag = document.getElementById('wv-note-model-tag');
|
|
if (noteEl) setOutputText(noteEl, data.note);
|
|
// Store source for refine — collect all well visit inputs
|
|
var wvSource = [];
|
|
var wvTranscript = document.getElementById('wv-transcript');
|
|
if (wvTranscript && wvTranscript.innerText.trim()) wvSource.push('TRANSCRIPT: ' + wvTranscript.innerText.trim());
|
|
var wvAge = document.getElementById('wv-note-age');
|
|
if (wvAge && wvAge.value) wvSource.push('Age: ' + wvAge.value);
|
|
storeSourceContext('wv-note-text', wvSource.join('\n'));
|
|
if (tag) tag.textContent = (data.model || '').split('/').pop();
|
|
if (outCard) { outCard.classList.remove('hidden'); outCard.scrollIntoView({ behavior: 'smooth' }); }
|
|
showToast('Well visit note generated!', 'success');
|
|
if (typeof suggestBillingCodes === 'function') suggestBillingCodes('wv-note-text', data.note, 'wellvisit', document.getElementById('wv-note-age').value);
|
|
} else {
|
|
showToast(data.error || 'Generation failed', 'error');
|
|
}
|
|
})
|
|
.catch(function(err) { hideBusy(); showToast(err.message, 'error'); });
|
|
}
|
|
|
|
// ── Wire subtab events ───────────────────────────────────────────────────
|
|
function initSubtabWiring() {
|
|
var subtabBar = document.querySelector('.wv-subtab-bar');
|
|
if (!subtabBar) return;
|
|
|
|
subtabBar.addEventListener('click', function(e) {
|
|
var btn = e.target.closest('.wv-subtab-btn');
|
|
if (!btn) return;
|
|
var subtab = btn.dataset.subtab;
|
|
if (subtab === 'shadess' || subtab === 'note') {
|
|
// These are handled here; wellVisit.js handles the others
|
|
showWvSubpanel(subtab);
|
|
}
|
|
});
|
|
}
|
|
|
|
function showWvSubpanel(name) {
|
|
document.querySelectorAll('.wv-subpanel').forEach(function(p) { p.classList.add('hidden'); });
|
|
document.querySelectorAll('.wv-subtab-btn').forEach(function(b) { b.classList.remove('active'); });
|
|
var panel = document.getElementById('wv-panel-' + name);
|
|
var btn = document.querySelector('.wv-subtab-btn[data-subtab="' + name + '"]');
|
|
if (panel) panel.classList.remove('hidden');
|
|
if (btn) btn.classList.add('active');
|
|
}
|
|
|
|
// ── Save bar for well visit ──────────────────────────────────────────────
|
|
document.addEventListener('click', function(e) {
|
|
if (e.target.closest('#btn-shadess-generate')) generateShadess();
|
|
if (e.target.closest('#btn-wv-generate')) generateWvNote();
|
|
if (e.target.closest('#wv-refine-btn')) refineDocument('wv-note-text', 'wv-refine-input');
|
|
if (e.target.closest('#wv-shorten-btn')) shortenDocument('wv-note-text');
|
|
if (e.target.closest('#btn-wv-save')) {
|
|
var labelEl = document.getElementById('wv-label');
|
|
var transcriptEl = document.getElementById('wv-transcript');
|
|
var noteEl = document.getElementById('wv-note-text');
|
|
if (typeof saveEncounter === 'function') {
|
|
window.saveEncounter({
|
|
id: window._savedEncId_wellvisit || null,
|
|
label: labelEl ? labelEl.value.trim() : '',
|
|
enc_type: 'wellvisit',
|
|
transcript: transcriptEl ? (transcriptEl.innerText || '') : '',
|
|
generated_note: noteEl ? (noteEl.innerText || '') : '',
|
|
onSaved: function(id) { window._savedEncId_wellvisit = id; }
|
|
});
|
|
}
|
|
}
|
|
if (e.target.closest('#btn-shadess-new-patient')) resetShadessForm();
|
|
if (e.target.closest('#btn-wv-new-patient')) resetWvNote();
|
|
});
|
|
|
|
// Register well visit load handler
|
|
if (typeof registerEncounterLoadHandler === 'function') {
|
|
registerEncounterLoadHandler('wellvisit', function(enc) {
|
|
var transcriptEl = document.getElementById('wv-transcript');
|
|
if (transcriptEl && enc.transcript) transcriptEl.textContent = enc.transcript;
|
|
if (enc.generated_note) {
|
|
var noteEl = document.getElementById('wv-note-text');
|
|
var outCard = document.getElementById('wv-note-output');
|
|
if (noteEl) noteEl.textContent = enc.generated_note;
|
|
if (outCard) outCard.classList.remove('hidden');
|
|
}
|
|
});
|
|
}
|
|
|
|
// ── Reset functions ──────────────────────────────────────────────────────
|
|
function resetShadessForm() {
|
|
// Reset answers
|
|
SHADESS_DOMAINS.forEach(function(d) {
|
|
_shadessAnswers[d.key] = { questions: {}, comment: '', concern: false, skipped: false };
|
|
});
|
|
// Clear the DOM form
|
|
initShadess();
|
|
// Clear age/gender inputs
|
|
var ageEl = document.getElementById('shadess-age');
|
|
var genderEl = document.getElementById('shadess-gender');
|
|
if (ageEl) ageEl.value = '';
|
|
if (genderEl) genderEl.value = '';
|
|
// Clear transcript and hide output
|
|
_shadessTranscript = '';
|
|
var outCard = document.getElementById('shadess-output');
|
|
var resultEl = document.getElementById('shadess-result-text');
|
|
if (outCard) outCard.classList.add('hidden');
|
|
if (resultEl) resultEl.textContent = '';
|
|
showToast('SSHADESS form cleared for new patient', 'info');
|
|
}
|
|
|
|
function resetWvNote() {
|
|
// Clear all well visit fields
|
|
['wv-note-age', 'wv-note-gender', 'wv-vitals', 'wv-screenings', 'wv-vaccines', 'wv-shadess-text', 'wv-label'].forEach(function(id) {
|
|
var el = document.getElementById(id);
|
|
if (el) el.value = '';
|
|
});
|
|
var transcriptEl = document.getElementById('wv-transcript');
|
|
if (transcriptEl) transcriptEl.textContent = '';
|
|
_wvTranscript = '';
|
|
|
|
// Reset ROS/PE/DX
|
|
_rosData = {};
|
|
_peData = {};
|
|
_wvSelectedDx.length = 0;
|
|
initWvRosPe();
|
|
|
|
var outCard = document.getElementById('wv-note-output');
|
|
var noteEl = document.getElementById('wv-note-text');
|
|
if (outCard) outCard.classList.add('hidden');
|
|
if (noteEl) noteEl.textContent = '';
|
|
window._savedEncId_wellvisit = null;
|
|
showToast('Well visit note cleared for new patient', 'info');
|
|
}
|
|
|
|
function esc(str) {
|
|
if (!str) return '';
|
|
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
}
|
|
|
|
// ── Init on DOM ready ────────────────────────────────────────────────────
|
|
var _inited = false;
|
|
document.addEventListener('tabChanged', function(e) {
|
|
if (e.detail.tab !== 'wellvisit' || _inited) return;
|
|
_inited = true;
|
|
initShadess();
|
|
initShadessRecording();
|
|
initWvRecording();
|
|
initSubtabWiring();
|
|
initWvRosPe();
|
|
});
|
|
|
|
console.log('SHADESS module loaded');
|
|
|
|
})();
|