All JS modules were running immediately (IIFE/DOMContentLoaded) but tab HTML is lazy-loaded from /components/*.html, causing null errors on every addEventListener call. Each module now listens for tabChanged with its tab name and initializes once after the DOM is ready. Also: quiz question/explanation boxes changed to textarea for multi-line support; quiz display uses sanitizeHtml() so formatted text (bold, lists, etc.) renders correctly.
1004 lines
47 KiB
JavaScript
1004 lines
47 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) {}
|
|
|
|
showLoading('Transcribing SSHADESS...');
|
|
_shadessRecorder.stop().then(function(blob) {
|
|
if (!blob || blob.size === 0) { hideLoading(); return; }
|
|
return transcribeAudio(blob).then(function(data) {
|
|
hideLoading();
|
|
if (data.success) {
|
|
_shadessTranscript = data.text;
|
|
showToast('SSHADESS audio transcribed (' + dur + 's)', 'success');
|
|
}
|
|
});
|
|
}).catch(function() { hideLoading(); });
|
|
}
|
|
});
|
|
|
|
if (pauseBtn) {
|
|
pauseBtn.addEventListener('click', function() {
|
|
if (!_shadessRecording || !_shadessRecorder || !_shadessRecorder.mediaRecorder) return;
|
|
if (!_shadessIsPaused) {
|
|
_shadessRecorder.mediaRecorder.pause();
|
|
_shadessTimer.stop();
|
|
_shadessIsPaused = true;
|
|
pauseBtn.innerHTML = '<i class="fas fa-play"></i> Resume';
|
|
if (_shhadessRecognition) try { _shhadessRecognition.stop(); } catch(e) {}
|
|
} else {
|
|
_shadessRecorder.mediaRecorder.resume();
|
|
_shadessTimer.start();
|
|
_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;
|
|
}
|
|
|
|
showLoading('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) {
|
|
hideLoading();
|
|
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) { hideLoading(); 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
|
|
window.wireRosContainer = function(containerEl, dataObj) {
|
|
containerEl.addEventListener('click', 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('input', 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;
|
|
});
|
|
};
|
|
|
|
// 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) {}
|
|
|
|
showLoading('Transcribing...');
|
|
_wvRecorder.stop().then(function(blob) {
|
|
if (!blob || blob.size === 0) { hideLoading(); if (_wvTranscript.trim() && transcriptEl) transcriptEl.textContent = _wvTranscript.trim(); return; }
|
|
return transcribeAudio(blob).then(function(data) {
|
|
hideLoading();
|
|
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() { hideLoading(); });
|
|
}
|
|
});
|
|
|
|
if (pauseBtn) {
|
|
pauseBtn.addEventListener('click', function() {
|
|
if (!_wvRecording || !_wvRecorder || !_wvRecorder.mediaRecorder) return;
|
|
if (!_wvPaused) {
|
|
_wvRecorder.mediaRecorder.pause(); _wvTimer.stop(); _wvPaused = true;
|
|
pauseBtn.innerHTML = '<i class="fas fa-play"></i> Resume';
|
|
if (_wvRecognition) try { _wvRecognition.stop(); } catch(e) {}
|
|
} else {
|
|
_wvRecorder.mediaRecorder.resume(); _wvTimer.start(); _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);
|
|
}
|
|
|
|
if (allWnlBtn) {
|
|
allWnlBtn.addEventListener('click', function() {
|
|
ROS_SYSTEMS.forEach(function(sys) { _rosData[sys.key + '_status'] = 'wnl'; });
|
|
if (rosContainer) {
|
|
rosContainer.innerHTML = window.renderRosRows(ROS_SYSTEMS, _rosData, 'ros', { wnl: 'WNL', abnormal: 'Abnormal', notrev: 'Not reviewed' });
|
|
window.wireRosContainer(rosContainer, _rosData);
|
|
}
|
|
showToast('All ROS set to WNL', 'success');
|
|
});
|
|
}
|
|
|
|
if (allNormalBtn) {
|
|
allNormalBtn.addEventListener('click', function() {
|
|
PE_SYSTEMS.forEach(function(sys) { _peData[sys.key + '_status'] = 'wnl'; });
|
|
if (peContainer) {
|
|
peContainer.innerHTML = window.renderRosRows(PE_SYSTEMS, _peData, 'pe', { wnl: 'Normal', abnormal: 'Abnormal', notrev: 'Not examined' });
|
|
window.wireRosContainer(peContainer, _peData);
|
|
}
|
|
showToast('All PE set to Normal', 'success');
|
|
});
|
|
}
|
|
|
|
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 || '') : '';
|
|
|
|
showLoading('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) {
|
|
hideLoading();
|
|
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);
|
|
if (tag) tag.textContent = (data.model || '').split('/').pop();
|
|
if (outCard) { outCard.classList.remove('hidden'); outCard.scrollIntoView({ behavior: 'smooth' }); }
|
|
showToast('Well visit note generated!', 'success');
|
|
} else {
|
|
showToast(data.error || 'Generation failed', 'error');
|
|
}
|
|
})
|
|
.catch(function(err) { hideLoading(); 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');
|
|
|
|
})();
|