pediatric-ai-scribe-v3/public/js/wellVisit.js
Daniel Onyejesi 5dd08f8da3 Feat: admin CMS, save/resume encounters, user templates, SSHADESS, well visit notes, sick visit, ROS/PE checklists, ICD-10 diagnoses
- Admin CMS: announcements, feature flags, email templates, AI prompts, SMTP, model management, reset-to-defaults
- Save/resume encounter progress with 7-day auto-expiry and patient labels
- Inline searchable load popover (replaces settings-modal redirect)
- User memory/template system (physical exam, ROS, encounter format, etc.)
- SSHADESS psychosocial screening form (8 domains, listen-in recording)
- Well visit note generation with vitals, vaccines, screenings, SSHADESS
- Interactive screenings/vaccines status buttons (Done/Refused/Already Given/N/A)
- ROS 15-system checklist + PE 17-system checklist in well visit note tab
- ICD-10 diagnoses tag picker (28 common pediatric codes + free text)
- Simple sick visit tab: chief complaint, inferred ROS (4) + PE (7), diagnoses, generate note
- Settings converted from modal to full-page tab
- Fix: /api/models moved before authenticated routes (was returning 401 → empty model selector)
- Fix: getUserMemoryContext always fetches from API regardless of cache state
- Pause/resume recording on all recording tabs
- Site-level SMTP config stored in DB; ElevenLabs TTS with browser fallback
2026-03-22 16:43:39 -04:00

535 lines
23 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ============================================================
// WELL VISIT / PREVENTIVE CARE TAB
// ============================================================
(function () {
'use strict';
// ─── Human-readable labels ─────────────────────────────────────────────────
var SCREEN_LABELS = {
maternalDepression: 'Maternal/Caregiver Depression Screen (Edinburgh/PHQ)',
developmentalScreening: 'Developmental Screening (ASQ / PEDS)',
autismScreening: 'Autism Screening (M-CHAT-R)',
developmentalSurveillance:'Developmental Surveillance',
behavioralScreening: 'Social-Emotional/Behavioral Screening (ASQ:SE)',
tobaccoAlcoholDrugs: 'Tobacco / Alcohol / Drug Use Screening (CRAFFT/AUDIT)',
depressionSuicideRisk: 'Depression & Suicide Risk Screening (PHQ-A)',
};
var PROC_LABELS = {
newbornBlood: 'Newborn Blood Spot Screening (NBS)',
newbornBilirubin: 'Newborn Bilirubin (TcB or TSB)',
criticalCHD: 'Critical CHD Screening (Pulse Ox)',
immunization: 'Immunizations Review & Update',
anemia: 'Anemia Screening (Hgb/Hct)',
lead: 'Lead Exposure Risk / Blood Lead Level',
tuberculosis: 'Tuberculosis / Latent TB Risk Assessment',
dyslipidemia: 'Dyslipidemia Screening (lipid panel)',
sti: 'STI Screening (gonorrhea / chlamydia / syphilis)',
hiv: 'HIV Screening',
hepB: 'Hepatitis B Screening (HBsAg)',
hepC: 'Hepatitis C Screening (anti-HCV)',
suddenCardiacArrest:'Sudden Cardiac Arrest Risk Assessment',
cervicalDysplasia: 'Cervical Dysplasia Screening (Pap smear)',
};
var MEASURE_LABELS = {
lengthHeight: 'Length / Height',
weight: 'Weight',
headCircumference: 'Head Circumference',
weightForLength: 'Weight-for-Length',
bmi: 'BMI',
bloodPressure: 'Blood Pressure',
};
var ORAL_LABELS = {
assessment: 'Oral Health Risk Assessment',
fluorideVarnish: 'Fluoride Varnish Application',
fluorideSupplementation:'Fluoride Supplementation (if water <0.6 ppm)',
};
var SENSORY_LABELS = {
vision: 'Vision Screening',
hearing: 'Hearing Screening',
};
var VACCINE_FULL_NAMES = {
HepB: 'Hepatitis B (HepB)',
RV: 'Rotavirus (RV)',
DTaP: 'DTaP (Diphtheria, Tetanus, Pertussis)',
Hib: 'Hib (Haemophilus influenzae type b)',
PCV: 'Pneumococcal (PCV)',
IPV: 'Polio (IPV)',
Flu_IIV: 'Influenza (IIV)',
MMR: 'MMR (Measles, Mumps, Rubella)',
VAR: 'Varicella (VAR)',
HepA: 'Hepatitis A (HepA)',
Tdap: 'Tdap (Tetanus, Diphtheria, Pertussis booster)',
HPV: 'HPV (Human Papillomavirus)',
MenACWY: 'Meningococcal ACWY (MenACWY)',
MenB: 'Meningococcal B (MenB)',
RSV_mAb: 'RSV Monoclonal Antibody (Nirsevimab/Beyfortus)',
COVID: 'COVID-19',
Dengue: 'Dengue (DEN4CYD / Dengvaxia)',
Mpox: 'Mpox (JYNNEOS)',
};
// ─── Module-level state for visit statuses ────────────────────────────────
// keyed by visitId + '.' + itemKey
var _visitStatuses = {};
// ─── Init ──────────────────────────────────────────────────────────────────
function init() {
populateVisitSelect();
renderCatchUp();
renderFullSchedule();
document.getElementById('wv-visit-select').addEventListener('change', onVisitChange);
document.querySelectorAll('.wv-subtab-btn').forEach(function (btn) {
btn.addEventListener('click', function () { switchSubtab(btn.dataset.subtab); });
});
// Show first real visit by default
var sel = document.getElementById('wv-visit-select');
if (sel && sel.value) onVisitChange();
}
function populateVisitSelect() {
var sel = document.getElementById('wv-visit-select');
if (!sel || typeof VISIT_AGES === 'undefined') return;
var grouped = {};
VISIT_AGES.forEach(function (v) {
if (!grouped[v.era]) grouped[v.era] = [];
grouped[v.era].push(v);
});
var eraNames = {
prenatal: 'Prenatal',
infancy: 'Infancy (012 mo)',
earlyChildhood: 'Early Childhood (15 y)',
middleChildhood: 'Middle Childhood (611 y)',
adolescence: 'Adolescence (1121 y)',
};
Object.keys(grouped).forEach(function (era) {
var og = document.createElement('optgroup');
og.label = eraNames[era] || era;
grouped[era].forEach(function (v) {
var opt = document.createElement('option');
opt.value = v.id;
opt.textContent = v.label;
og.appendChild(opt);
});
sel.appendChild(og);
});
// select newborn by default
sel.value = 'newborn';
}
function onVisitChange() {
var visitId = document.getElementById('wv-visit-select').value;
if (!visitId) return;
renderVisitPanel(visitId);
}
function switchSubtab(name) {
document.querySelectorAll('.wv-subtab-btn').forEach(function (b) {
b.classList.toggle('active', b.dataset.subtab === name);
});
document.querySelectorAll('.wv-subpanel').forEach(function (p) {
p.classList.toggle('hidden', p.id !== 'wv-panel-' + name);
});
}
// ─── BY VISIT panel ───────────────────────────────────────────────────────
function renderVisitPanel(visitId) {
var data = (typeof PERIODICITY !== 'undefined') ? PERIODICITY[visitId] : null;
var codes = (typeof WELL_VISIT_CODES !== 'undefined') ? WELL_VISIT_CODES[visitId] : null;
var panel = document.getElementById('wv-visit-detail');
if (!panel) return;
if (!data) {
panel.innerHTML = '<p class="wv-empty">No data for this visit.</p>';
return;
}
var html = '';
// ── Billing codes ──
if (codes) {
html += '<div class="wv-section wv-billing">';
html += '<h4 class="wv-section-title"><i class="fas fa-receipt"></i> Billing Codes</h4>';
html += '<div class="wv-billing-grid">';
html += '<div class="wv-billing-cell"><span class="wv-label">ICD-10</span><span class="wv-code-chip icd">' + esc(codes.icd10) + '</span></div>';
html += '<div class="wv-billing-cell"><span class="wv-label">CPT</span><span class="wv-code-chip cpt">' + esc(codes.cpt) + '</span></div>';
html += '<div class="wv-billing-desc">' + esc(codes.description) + '</div>';
html += '</div></div>';
}
// ── Measurements ──
var measDue = Object.keys(data.measurements || {}).filter(function (k) {
return data.measurements[k] === 'dot' || data.measurements[k] === 'range';
});
if (measDue.length) {
html += '<div class="wv-section">';
html += '<h4 class="wv-section-title"><i class="fas fa-ruler"></i> Measurements</h4>';
html += '<div class="wv-chip-list">';
measDue.forEach(function (k) {
var isRange = data.measurements[k] === 'range';
html += '<span class="wv-chip wv-chip-measure' + (isRange ? ' wv-chip-range' : '') + '">' + esc(MEASURE_LABELS[k] || k) + (isRange ? ' <em>(range)</em>' : '') + '</span>';
});
html += '</div></div>';
}
// ── Vaccines ──
if (data.vaccines && data.vaccines.length) {
html += '<div class="wv-section">';
html += '<h4 class="wv-section-title"><i class="fas fa-syringe"></i> Vaccines Due</h4>';
html += '<div class="wv-vax-list">';
data.vaccines.forEach(function (v) {
var fullName = VACCINE_FULL_NAMES[v.vaccine] || v.vaccine;
var itemKey = 'vax_' + v.vaccine + '_d' + (v.dose || '');
var statusKey = visitId + '.' + itemKey;
var curStatus = _visitStatuses[statusKey] || '';
html += '<div class="wv-vax-item" data-status-key="' + esc(statusKey) + '" data-item-type="vaccine" data-item-label="' + esc(fullName + (v.dose ? ' Dose ' + v.dose : '')) + '">';
html += '<div class="wv-vax-name"><i class="fas fa-circle-dot wv-vax-dot"></i>' + esc(fullName) + '</div>';
if (v.dose) html += '<span class="wv-vax-dose">Dose ' + esc(String(v.dose)) + '</span>';
if (v.notes) html += '<div class="wv-vax-note"><i class="fas fa-circle-info"></i> ' + esc(v.notes) + '</div>';
html += '<div class="wv-vax-status-row" style="display:flex;gap:4px;flex-wrap:wrap;margin-top:4px;">';
html += renderVaxStatusBtns(statusKey, curStatus);
html += '</div>';
html += '</div>';
});
html += '</div></div>';
}
// ── Screenings: sensory ──
var sensoryItems = buildStatusItems(data.sensory || {}, SENSORY_LABELS);
if (sensoryItems.length) {
html += '<div class="wv-section">';
html += '<h4 class="wv-section-title"><i class="fas fa-eye"></i> Sensory Screens</h4>';
html += renderScreenItems(sensoryItems, visitId);
html += '</div>';
}
// ── Screenings: developmental / behavioral ──
var devItems = buildStatusItems(data.developmental || {}, SCREEN_LABELS);
if (devItems.length) {
html += '<div class="wv-section">';
html += '<h4 class="wv-section-title"><i class="fas fa-brain"></i> Developmental / Behavioral Screens</h4>';
html += renderScreenItems(devItems, visitId);
html += '</div>';
}
// ── Procedures ──
var procItems = buildStatusItems(data.procedures || {}, PROC_LABELS, ['immunization']);
if (procItems.length) {
html += '<div class="wv-section">';
html += '<h4 class="wv-section-title"><i class="fas fa-flask"></i> Labs & Procedures</h4>';
html += renderScreenItems(procItems, visitId);
html += '</div>';
}
// ── Oral Health ──
var oralItems = buildStatusItems(data.oralHealth || {}, ORAL_LABELS);
if (oralItems.length) {
html += '<div class="wv-section">';
html += '<h4 class="wv-section-title"><i class="fas fa-tooth"></i> Oral Health</h4>';
html += renderScreenItems(oralItems, visitId);
html += '</div>';
}
// ── Notes ──
if (data.notes) {
html += '<div class="wv-section wv-notes-box"><i class="fas fa-circle-info"></i> ' + esc(data.notes) + '</div>';
}
// ── Copy to Visit Note button ──
html += '<div style="margin-top:12px;padding:8px 0;">';
html += '<button class="btn-sm btn-primary" id="btn-wv-copy-to-note" style="font-size:12px;"><i class="fas fa-clipboard-check"></i> Copy Statuses to Visit Note</button>';
html += '<span style="font-size:11px;color:var(--g400);margin-left:8px;">Copies vaccine + screening statuses into the Screenings & Vaccines fields</span>';
html += '</div>';
panel.innerHTML = html || '<p class="wv-empty">No specific recommendations found for this visit.</p>';
// Wire up status buttons
panel.addEventListener('click', function (e) {
var btn = e.target.closest('.visit-status-btn');
if (btn) handleVisitStatusClick(btn, visitId);
if (e.target.closest('#btn-wv-copy-to-note')) {
copyVisitStatusesToNote(visitId);
}
});
// Wire up notes inputs
panel.addEventListener('input', function (e) {
var noteInput = e.target.closest('.visit-note-input');
if (noteInput) {
var sk = noteInput.dataset.statusKey;
if (!_visitStatuses[sk]) _visitStatuses[sk] = {};
if (typeof _visitStatuses[sk] === 'string') _visitStatuses[sk] = { status: _visitStatuses[sk] };
_visitStatuses[sk].note = noteInput.value;
}
});
}
function renderVaxStatusBtns(statusKey, curStatus) {
var statuses = [
{ value: 'Given', label: 'Given', cls: 'given' },
{ value: 'Refused', label: 'Refused', cls: 'refused' },
{ value: 'Deferred', label: 'Deferred', cls: 'deferred' },
{ value: 'Already Done', label: 'Already Done', cls: 'already-done' },
];
return statuses.map(function (s) {
var active = (typeof curStatus === 'object' ? curStatus.status : curStatus) === s.value;
return '<button class="visit-status-btn' + (active ? ' ' + s.cls : '') + '" data-status-key="' + esc(statusKey) + '" data-status-val="' + esc(s.value) + '" data-btn-cls="' + s.cls + '">' + s.label + '</button>';
}).join('');
}
function renderScreenStatusBtns(statusKey, curStatus) {
var statuses = [
{ value: 'Done', label: 'Done', cls: 'done' },
{ value: 'Refused', label: 'Refused', cls: 'refused' },
{ value: 'Not Due / N/A', label: 'Not Due / N/A', cls: 'not-due' },
];
return statuses.map(function (s) {
var active = (typeof curStatus === 'object' ? curStatus.status : curStatus) === s.value;
return '<button class="visit-status-btn' + (active ? ' ' + s.cls : '') + '" data-status-key="' + esc(statusKey) + '" data-status-val="' + esc(s.value) + '" data-btn-cls="' + s.cls + '">' + s.label + '</button>';
}).join('');
}
function handleVisitStatusClick(btn, visitId) {
var statusKey = btn.dataset.statusKey;
var statusVal = btn.dataset.statusVal;
var btnCls = btn.dataset.btnCls;
if (!statusKey) return;
// Toggle: clicking same status deselects
var cur = _visitStatuses[statusKey];
var curVal = typeof cur === 'object' ? (cur ? cur.status : '') : (cur || '');
var note = typeof cur === 'object' && cur ? (cur.note || '') : '';
if (curVal === statusVal) {
_visitStatuses[statusKey] = { status: '', note: note };
} else {
_visitStatuses[statusKey] = { status: statusVal, note: note };
}
// Update button highlight in this row
var container = btn.closest('[data-status-key]') || btn.parentElement;
// Find all visit-status-btn siblings with same statusKey
var parent = btn.parentElement;
parent.querySelectorAll('.visit-status-btn').forEach(function (b) {
b.className = 'visit-status-btn';
});
var newVal = _visitStatuses[statusKey].status;
if (newVal) {
parent.querySelectorAll('.visit-status-btn').forEach(function (b) {
if (b.dataset.statusVal === newVal) {
b.classList.add(b.dataset.btnCls);
}
});
}
}
function copyVisitStatusesToNote(visitId) {
var lines = [];
// Vaccines
var vaxLines = [];
Object.keys(_visitStatuses).forEach(function (k) {
if (k.indexOf(visitId + '.vax_') === 0) {
var itemEl = document.querySelector('[data-status-key="' + k + '"]');
var label = itemEl ? itemEl.dataset.itemLabel : k.replace(visitId + '.vax_', '');
var s = _visitStatuses[k];
var status = typeof s === 'object' ? s.status : s;
var note = typeof s === 'object' ? (s.note || '') : '';
if (status) {
vaxLines.push(label + ': ' + status + (note ? ' (' + note + ')' : ''));
}
}
});
if (vaxLines.length) {
lines.push('Vaccines:');
vaxLines.forEach(function (l) { lines.push(' - ' + l); });
}
// Screenings
var screenLines = [];
Object.keys(_visitStatuses).forEach(function (k) {
if (k.indexOf(visitId + '.') === 0 && k.indexOf(visitId + '.vax_') !== 0) {
var itemEl = document.querySelector('[data-status-key="' + k + '"]');
var label = itemEl ? itemEl.dataset.itemLabel : k.replace(visitId + '.', '');
var s = _visitStatuses[k];
var status = typeof s === 'object' ? s.status : s;
var note = typeof s === 'object' ? (s.note || '') : '';
if (status) {
screenLines.push(label + ': ' + status + (note ? ' (' + note + ')' : ''));
}
}
});
if (screenLines.length) {
lines.push('Screenings:');
screenLines.forEach(function (l) { lines.push(' - ' + l); });
}
if (!lines.length) {
if (typeof showToast === 'function') showToast('No statuses selected yet. Click Done/Given/etc on each item first.', 'info');
return;
}
var text = lines.join('\n');
// Put into wv-vaccines and wv-screenings textareas
var vaxEl = document.getElementById('wv-vaccines');
var screenEl = document.getElementById('wv-screenings');
if (vaxLines.length && vaxEl) {
var existing = vaxEl.value.trim();
vaxEl.value = existing ? existing + '\n' + vaxLines.join('\n') : vaxLines.join('\n');
}
if (screenLines.length && screenEl) {
var existingS = screenEl.value.trim();
screenEl.value = existingS ? existingS + '\n' + screenLines.join('\n') : screenLines.join('\n');
}
if (typeof showToast === 'function') showToast('Visit statuses copied to Visit Note tab', 'success');
}
function buildStatusItems(obj, labels, exclude) {
exclude = exclude || [];
return Object.keys(obj).filter(function (k) {
return obj[k] && obj[k] !== '' && exclude.indexOf(k) === -1;
}).map(function (k) {
return { key: k, status: obj[k], label: labels[k] || k };
});
}
function renderScreenItems(items, visitId) {
var html = '<div class="wv-screen-list">';
items.forEach(function (item) {
var statusClass = item.status === 'dot' ? 'wv-status-dot' : item.status === 'range' ? 'wv-status-range' : 'wv-status-risk';
var statusText = item.status === 'dot' ? 'Recommended' : item.status === 'range' ? 'In Range' : 'If at Risk';
var statusKey = (visitId || 'visit') + '.' + item.key;
var curStatus = _visitStatuses[statusKey] || {};
var curVal = typeof curStatus === 'object' ? (curStatus.status || '') : (curStatus || '');
var curNote = typeof curStatus === 'object' ? (curStatus.note || '') : '';
html += '<div class="wv-screen-item" data-status-key="' + esc(statusKey) + '" data-item-label="' + esc(item.label) + '">';
html += '<span class="wv-status-badge ' + statusClass + '">' + statusText + '</span>';
html += '<span class="wv-screen-name">' + esc(item.label) + '</span>';
html += '<div style="display:flex;gap:4px;align-items:center;flex-wrap:wrap;margin-left:auto;">';
html += renderScreenStatusBtns(statusKey, curVal);
html += '<input class="visit-note-input" data-status-key="' + esc(statusKey) + '" type="text" placeholder="Notes..." style="font-size:11px;padding:2px 6px;border:1px solid var(--g300);border-radius:6px;width:120px;" value="' + esc(curNote) + '">';
html += '</div>';
html += '</div>';
});
html += '</div>';
return html;
}
// ─── CATCH-UP panel ───────────────────────────────────────────────────────
function renderCatchUp() {
var panel = document.getElementById('wv-panel-catchup');
if (!panel || typeof CATCH_UP_SCHEDULE === 'undefined') return;
var html = '<div class="wv-catchup-intro"><i class="fas fa-circle-info"></i> CDC 2025 Catch-up Schedule — minimum ages and intervals for each vaccine series. Applies when a child is behind on routine immunizations.</div>';
Object.keys(CATCH_UP_SCHEDULE).forEach(function (vaxKey) {
var v = CATCH_UP_SCHEDULE[vaxKey];
var fullName = VACCINE_FULL_NAMES[vaxKey] || vaxKey;
html += '<div class="wv-catchup-card">';
html += '<h4 class="wv-catchup-title"><i class="fas fa-syringe"></i> ' + esc(fullName) + '</h4>';
if (v.minimumAgeForDose1) {
html += '<div class="wv-catchup-min-age">Minimum age for dose 1: <strong>' + esc(v.minimumAgeForDose1) + '</strong></div>';
}
if (v.series && v.series.length) {
html += '<table class="wv-catchup-table"><thead><tr><th>Dose</th><th>Min Age</th><th>Min Interval from Prev</th><th>Notes</th></tr></thead><tbody>';
v.series.forEach(function (s) {
html += '<tr>';
html += '<td><strong>Dose ' + esc(String(s.dose)) + '</strong></td>';
html += '<td>' + esc(s.minimumAge || '—') + '</td>';
html += '<td>' + esc(s.minimumIntervalToPrev || '—') + '</td>';
html += '<td>' + esc(s.notes || '') + '</td>';
html += '</tr>';
});
html += '</tbody></table>';
}
if (v.catchUpNotes) {
var notes = Array.isArray(v.catchUpNotes) ? v.catchUpNotes : [v.catchUpNotes];
html += '<ul class="wv-catchup-notes">';
notes.forEach(function (n) {
html += '<li>' + esc(n) + '</li>';
});
html += '</ul>';
}
html += '</div>';
});
panel.innerHTML = html;
}
// ─── FULL SCHEDULE table ──────────────────────────────────────────────────
function renderFullSchedule() {
var panel = document.getElementById('wv-panel-schedule');
if (!panel || typeof PERIODICITY === 'undefined' || typeof VISIT_AGES === 'undefined') return;
// Collect all vaccine keys that appear in any visit
var allVaxKeys = {};
var visitIds = VISIT_AGES.map(function (v) { return v.id; }).filter(function (id) {
return PERIODICITY[id] && PERIODICITY[id].vaccines;
});
visitIds.forEach(function (id) {
PERIODICITY[id].vaccines.forEach(function (v) { allVaxKeys[v.vaccine] = true; });
});
var vaxKeys = Object.keys(allVaxKeys);
var visitLabels = VISIT_AGES.filter(function (v) { return visitIds.indexOf(v.id) !== -1; });
var html = '<div class="wv-schedule-scroll">';
html += '<table class="wv-schedule-table">';
html += '<thead><tr><th class="wv-sched-vax-col">Vaccine</th>';
visitLabels.forEach(function (v) {
html += '<th class="wv-sched-age-col">' + esc(v.label) + '</th>';
});
html += '</tr></thead><tbody>';
vaxKeys.forEach(function (vaxKey) {
var fullName = VACCINE_FULL_NAMES[vaxKey] || vaxKey;
html += '<tr><td class="wv-sched-vax-name">' + esc(fullName) + '</td>';
visitLabels.forEach(function (visit) {
var vaxData = PERIODICITY[visit.id] && PERIODICITY[visit.id].vaccines;
var match = vaxData && vaxData.find(function (v) { return v.vaccine === vaxKey; });
if (match) {
var dose = match.dose ? String(match.dose) : '•';
var label = (typeof match.dose === 'number') ? '#' + dose : dose;
html += '<td class="wv-sched-cell wv-sched-has" title="' + esc(match.notes || fullName + ' dose ' + dose) + '">' + esc(label) + '</td>';
} else {
html += '<td class="wv-sched-cell"></td>';
}
});
html += '</tr>';
});
html += '</tbody></table></div>';
html += '<p class="wv-sched-legend"><span class="wv-sched-leg-dot"></span> Dose number shown. Hover cells for notes. "#N" = routine dose N. "annual" / "seasonal" = per CDC guidance. "catch-up" = as needed.</p>';
panel.innerHTML = html;
}
function esc(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
// ─── Wire up ───────────────────────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', function () {
init();
});
})();