pediatric-ai-scribe-v3/public/js/wellVisit.js
Daniel Onyejesi 90c7bc35af Feat: milestones under well visit, standalone schedule tabs, remove admin models
- Move Milestones from standalone tab to well visit subtab (wv-panel-milestones)
  with 'Copy to Note' button that fills Developmental Assessment card in Visit Note
- Sidebar Milestones button now navigates to Well Visit → Milestones subtab
- Move Full Vaccine Schedule → standalone sidebar tab (vaxschedule-tab)
- Move Catch-Up Schedule → standalone sidebar tab (catchup-tab)
- Add Developmental Assessment textarea (wv-milestones-text) to Visit Note panel
- Remove AI Models management from admin panel (was failing to load)
- switchSubtab scoped to wellvisit-tab so standalone schedule panels unaffected
- Expose window.wvSwitchSubtab for cross-module subtab switching
2026-03-22 18:52:11 -04:00

596 lines
26 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;
// ─── 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>';
}
// ── 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 ───────────────────────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', function () {
init();
});
})();