pediatric-ai-scribe-v3/public/js/wellVisit.js
Daniel Onyejesi a69662d800 Fix vaccine/catchup tabs and serve Quill locally
- wellVisit.js now handles tabChanged for vaxschedule and catchup tabs
  so renderFullSchedule() and renderCatchUp() fire when those tabs open
- Replace Quill CDN with local /vendor/quill.min.js + quill.snow.css
  (CDN was unreachable in local/offline environments)
2026-03-23 22:59:47 -04:00

663 lines
30 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 — persisted to localStorage
var _visitStatuses = {};
// SSHADESS is only relevant for 12+ year visits
var SSHADESS_VISITS = ['12y','13y','14y','15y','16y','17y','18y','19y','20y','21y'];
function saveStatusesToStorage() {
try { localStorage.setItem('ped_visit_statuses', JSON.stringify(_visitStatuses)); } catch(e) {}
}
function clearCurrentVisit() {
var visitId = document.getElementById('wv-visit-select').value;
if (!visitId) return;
Object.keys(_visitStatuses).forEach(function(k) {
if (k.indexOf(visitId + '.') === 0) delete _visitStatuses[k];
});
saveStatusesToStorage();
renderVisitPanel(visitId);
if (typeof showToast === 'function') showToast('Visit statuses cleared', 'info');
}
// ─── Init ──────────────────────────────────────────────────────────────────
function init() {
// Restore statuses from localStorage
try {
var saved = localStorage.getItem('ped_visit_statuses');
if (saved) _visitStatuses = JSON.parse(saved) || {};
} catch(e) { _visitStatuses = {}; }
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); });
});
// Wire the visit-detail panel click/input ONCE here (not inside renderVisitPanel)
// which would accumulate a new listener on every visit change — causing "can't click Done"
var panel = document.getElementById('wv-visit-detail');
if (panel) {
panel.addEventListener('click', function (e) {
var visitId = document.getElementById('wv-visit-select').value;
var btn = e.target.closest('.visit-status-btn');
if (btn) { handleVisitStatusClick(btn, visitId); saveStatusesToStorage(); }
if (e.target.closest('#btn-wv-copy-to-note')) copyVisitStatusesToNote(visitId);
if (e.target.closest('#btn-wv-clear-visit')) clearCurrentVisit();
});
panel.addEventListener('input', function (e) {
var noteInput = e.target.closest('.visit-note-input');
if (noteInput) {
var sk = noteInput.dataset.statusKey;
if (!_visitStatuses[sk] || typeof _visitStatuses[sk] === 'string') {
_visitStatuses[sk] = { status: _visitStatuses[sk] || '', note: '' };
}
_visitStatuses[sk].note = noteInput.value;
saveStatusesToStorage();
}
});
}
// 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;
// Share selected visit age globally so other tabs can use it
var sel = document.getElementById('wv-visit-select');
var selectedOption = sel ? sel.options[sel.selectedIndex] : null;
window._wellVisitAge = selectedOption ? selectedOption.textContent : visitId;
try { sessionStorage.setItem('ped_visit_age', window._wellVisitAge); } catch(e) {}
renderVisitPanel(visitId);
// Show SSHADESS subtab only for age 12+ visits
var shadessBtn = document.querySelector('.wv-subtab-btn[data-subtab="shadess"]');
if (shadessBtn) {
var showShadess = SSHADESS_VISITS.indexOf(visitId) !== -1;
shadessBtn.style.display = showShadess ? '' : 'none';
// If SSHADESS was active but should now be hidden, fall back to byvisit
if (!showShadess && shadessBtn.classList.contains('active')) {
switchSubtab('byvisit');
}
}
// Milestones subtab is always shown (not age-gated)
var milestonesBtn = document.querySelector('.wv-subtab-btn[data-subtab="milestones"]');
if (milestonesBtn) {
milestonesBtn.style.display = '';
}
}
function switchSubtab(name) {
document.querySelectorAll('.wv-subtab-btn').forEach(function (b) {
b.classList.toggle('active', b.dataset.subtab === name);
});
(document.getElementById('wellvisit-tab') || document).querySelectorAll('.wv-subpanel').forEach(function (p) {
p.classList.toggle('hidden', p.id !== 'wv-panel-' + name);
});
}
window.wvSwitchSubtab = switchSubtab;
// ─── Map visit ID to GROWTH_REFERENCE key ─────────────────────────────────
function getGrowthDataForVisit(visitId) {
if (typeof GROWTH_REFERENCE === 'undefined') return null;
// Direct match
if (GROWTH_REFERENCE[visitId]) return GROWTH_REFERENCE[visitId];
// Map visit IDs to growth reference keys
var mapping = {
'newborn': 'newborn', '3-5d': 'newborn',
'1mo': '1mo', '2mo': '2mo', '4mo': '4mo', '6mo': '6mo', '9mo': '9mo',
'12mo': '12mo', '15mo': '15mo', '18mo': '18mo',
'24mo': '24mo', '30mo': '30mo',
'3y': '3y', '4y': '4y', '5y': '5y',
'6y': '6y_to_10y', '7y': '6y_to_10y', '8y': '6y_to_10y', '9y': '6y_to_10y', '10y': '6y_to_10y',
'11y': '11y_to_14y', '12y': '11y_to_14y', '13y': '11y_to_14y', '14y': '11y_to_14y',
'15y': '15y_to_21y', '16y': '15y_to_21y', '17y': '15y_to_21y', '18y': '15y_to_21y',
'19y': '15y_to_21y', '20y': '15y_to_21y', '21y': '15y_to_21y'
};
var key = mapping[visitId];
return key ? GROWTH_REFERENCE[key] : null;
}
// ─── 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>';
}
// ── Growth Reference & Feeding Guidance ──
var growthData = getGrowthDataForVisit(visitId);
if (growthData) {
html += '<div class="wv-section">';
html += '<h4 class="wv-section-title"><i class="fas fa-chart-line"></i> Expected Growth</h4>';
html += '<div class="wv-growth-box">';
if (growthData.weight) html += '<div class="wv-growth-item"><span class="wv-growth-label">⚖️ Weight</span><span>' + esc(growthData.weight) + '</span></div>';
if (growthData.length) html += '<div class="wv-growth-item"><span class="wv-growth-label">📏 Length/Height</span><span>' + esc(growthData.length) + '</span></div>';
if (growthData.headCirc) html += '<div class="wv-growth-item"><span class="wv-growth-label">🧠 Head Circumference</span><span>' + esc(growthData.headCirc) + '</span></div>';
html += '</div>';
if (growthData.feeding && growthData.feeding.length) {
html += '<h4 class="wv-section-title" style="margin-top:8px;"><i class="fas fa-utensils"></i> Feeding & Nutrition Guidance</h4>';
html += '<ul class="wv-feeding-list">';
growthData.feeding.forEach(function (f) {
html += '<li>' + esc(f) + '</li>';
});
html += '</ul>';
}
html += '</div>';
}
// ── BMI / Weight Classification (ages 2+) ──
if (growthData && growthData.bmiClassification && typeof BMI_CLASSIFICATION !== 'undefined') {
html += '<div class="wv-section">';
html += '<h4 class="wv-section-title"><i class="fas fa-weight-scale"></i> BMI / Weight Classification (AAP 2023)</h4>';
html += '<div class="wv-bmi-table">';
BMI_CLASSIFICATION.categories.forEach(function (cat) {
html += '<div class="wv-bmi-row" style="border-left:4px solid ' + cat.color + ';">';
html += '<div class="wv-bmi-label"><strong>' + esc(cat.label) + '</strong></div>';
html += '<div class="wv-bmi-range">' + esc(cat.range) + '</div>';
html += '<div class="wv-bmi-action">' + esc(cat.action) + '</div>';
html += '</div>';
});
html += '</div>';
html += '<div class="wv-bmi-note"><i class="fas fa-circle-info"></i> ' + esc(BMI_CLASSIFICATION.notes) + '</div>';
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>';
}
// ── Action buttons ──
html += '<div style="margin-top:12px;padding:8px 0;display:flex;align-items:center;gap:8px;flex-wrap:wrap;">';
html += '<button class="btn-sm btn-primary" id="btn-wv-copy-to-note"><i class="fas fa-clipboard-check"></i> Copy to Visit Note</button>';
html += '<button class="btn-sm btn-ghost" id="btn-wv-clear-visit" style="color:var(--red);"><i class="fas fa-rotate-left"></i> Clear This Visit</button>';
html += '<span style="font-size:11px;color:var(--g400);">Copies vaccine + screening statuses to the Visit Note tab</span>';
html += '</div>';
panel.innerHTML = html || '<p class="wv-empty">No specific recommendations found for this visit.</p>';
// Note: click/input listeners are wired once in init() — NOT here
}
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');
}
// Also copy SSHADESS assessment if present
var shadessText = document.getElementById('shadess-result-text');
var wvShadessEl = document.getElementById('wv-shadess-text');
if (shadessText && wvShadessEl) {
var shadessContent = (shadessText.innerText || shadessText.textContent || '').trim();
if (shadessContent) {
var existingSh = wvShadessEl.value.trim();
wvShadessEl.value = existingSh ? existingSh + '\n\n' + shadessContent : shadessContent;
}
}
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 ───────────────────────────────────────────────────────────────
var _inited = false;
var _vaxInited = false;
var _catchupInited = false;
document.addEventListener('tabChanged', function(e) {
var tab = e.detail.tab;
if (tab === 'wellvisit' && !_inited) { _inited = true; init(); }
if (tab === 'vaxschedule' && !_vaxInited) { _vaxInited = true; renderFullSchedule(); }
if (tab === 'catchup' && !_catchupInited) { _catchupInited = true; renderCatchUp(); }
});
})();