Feat: add Well Visit / Preventive Care tab with AAP 2025 data

- Add Well Visit tab with three sub-views: By Visit Age, Full Vaccine Schedule, Catch-Up Schedule
- By Visit Age: shows ICD-10/CPT billing codes, vaccines due with dose info, measurements, sensory/developmental/behavioral/procedure screenings, oral health, notes
- Full Schedule: scrollable table of all vaccines vs all visit ages
- Catch-Up Schedule: per-vaccine minimum ages, intervals, and catch-up notes per CDC 2025
- Add pediatricScheduleData.js (1719-line AAP 2025 Bright Futures dataset)
- Add wellVisit.js for tab logic (lazy-initialized on first tab activation)
- Fire tabChanged CustomEvent on tab switch for lazy initialization
- Add Well Visit CSS styles to styles.css
This commit is contained in:
Daniel Onyejesi 2026-03-21 23:19:24 -04:00
parent 1207202f45
commit 2b73db1664
5 changed files with 2218 additions and 0 deletions

View file

@ -267,3 +267,81 @@ body{font-family:'Inter',system-ui,sans-serif;background:var(--g50);color:var(--
.admin-table td{padding:10px 16px;border-bottom:1px solid var(--g100);vertical-align:middle;}
.admin-table tr:last-child td{border-bottom:none;}
.admin-table tr:hover td{background:var(--g50);}
/* ── WELL VISIT TAB ────────────────────────────────────────────────────────── */
.wv-subtab-bar{display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap;}
.wv-subtab-btn{padding:8px 18px;border:1.5px solid var(--g300);border-radius:8px;background:white;color:var(--g600);font-size:13px;font-weight:500;cursor:pointer;font-family:inherit;display:flex;align-items:center;gap:6px;transition:all 0.15s;}
.wv-subtab-btn:hover{border-color:var(--blue);color:var(--blue);}
.wv-subtab-btn.active{background:var(--blue);border-color:var(--blue);color:white;}
.wv-subpanel{animation:fadeIn 0.2s;}
.wv-visit-selector-row{display:flex;align-items:center;gap:12px;margin-bottom:20px;flex-wrap:wrap;}
.wv-visit-selector-row label{font-weight:600;font-size:14px;color:var(--g700);display:flex;align-items:center;gap:6px;}
.wv-visit-select{padding:8px 14px;border:1.5px solid var(--g300);border-radius:8px;font-size:14px;font-family:inherit;color:var(--g800);background:white;cursor:pointer;min-width:200px;}
.wv-visit-select:focus{outline:none;border-color:var(--blue);box-shadow:0 0 0 3px var(--blue-light);}
.wv-visit-detail{display:flex;flex-direction:column;gap:14px;}
.wv-section{background:white;border-radius:var(--radius);box-shadow:var(--shadow);padding:16px 20px;}
.wv-section-title{font-size:14px;font-weight:700;color:var(--g700);margin-bottom:12px;display:flex;align-items:center;gap:8px;}
.wv-section-title i{color:var(--blue);}
/* Billing */
.wv-billing-grid{display:flex;flex-wrap:wrap;gap:12px;align-items:center;}
.wv-billing-cell{display:flex;align-items:center;gap:8px;}
.wv-label{font-size:12px;font-weight:600;color:var(--g500);text-transform:uppercase;letter-spacing:0.5px;}
.wv-code-chip{padding:4px 12px;border-radius:20px;font-size:13px;font-weight:700;font-family:monospace;}
.wv-code-chip.icd{background:var(--blue-light);color:var(--blue-dark);}
.wv-code-chip.cpt{background:var(--purple-light);color:var(--purple);}
.wv-billing-desc{width:100%;font-size:13px;color:var(--g600);margin-top:4px;}
/* Chips */
.wv-chip-list{display:flex;flex-wrap:wrap;gap:8px;}
.wv-chip{padding:4px 12px;border-radius:20px;font-size:12px;font-weight:600;}
.wv-chip-measure{background:var(--teal-light);color:var(--teal);}
.wv-chip-range{background:var(--amber-light);color:#92400e;}
/* Vaccines */
.wv-vax-list{display:flex;flex-direction:column;gap:10px;}
.wv-vax-item{background:var(--g50);border-radius:8px;padding:10px 14px;border-left:3px solid var(--green);}
.wv-vax-name{font-size:14px;font-weight:600;color:var(--g800);display:flex;align-items:center;gap:8px;}
.wv-vax-dot{color:var(--green);font-size:10px;}
.wv-vax-dose{display:inline-block;margin-top:4px;font-size:11px;font-weight:700;padding:2px 10px;background:var(--green-light);color:#065f46;border-radius:10px;}
.wv-vax-note{margin-top:6px;font-size:12px;color:var(--g500);display:flex;align-items:flex-start;gap:6px;}
.wv-vax-note i{color:var(--blue);margin-top:2px;flex-shrink:0;}
/* Screens */
.wv-screen-list{display:flex;flex-direction:column;gap:8px;}
.wv-screen-item{display:flex;align-items:center;gap:10px;}
.wv-status-badge{flex-shrink:0;font-size:11px;font-weight:700;padding:2px 10px;border-radius:10px;text-transform:uppercase;letter-spacing:0.4px;}
.wv-status-dot{background:var(--green-light);color:#065f46;}
.wv-status-risk{background:var(--amber-light);color:#92400e;}
.wv-status-range{background:var(--blue-light);color:var(--blue-dark);}
.wv-screen-name{font-size:13px;color:var(--g700);}
/* Notes */
.wv-notes-box{background:var(--blue-light);border-radius:8px;padding:12px 16px;font-size:13px;color:#1e40af;display:flex;align-items:flex-start;gap:8px;}
.wv-empty,.wv-loading{color:var(--g400);font-size:14px;padding:20px 0;text-align:center;}
/* Catch-up */
.wv-catchup-intro{background:var(--blue-light);border-radius:8px;padding:12px 16px;font-size:13px;color:#1e40af;display:flex;align-items:flex-start;gap:8px;margin-bottom:16px;}
.wv-catchup-card{background:white;border-radius:var(--radius);box-shadow:var(--shadow);padding:16px 20px;margin-bottom:14px;}
.wv-catchup-title{font-size:14px;font-weight:700;color:var(--g800);margin-bottom:10px;display:flex;align-items:center;gap:8px;}
.wv-catchup-title i{color:var(--green);}
.wv-catchup-min-age{font-size:13px;color:var(--g600);margin-bottom:10px;}
.wv-catchup-table{width:100%;border-collapse:collapse;font-size:12px;margin-bottom:10px;}
.wv-catchup-table th{text-align:left;padding:6px 10px;background:var(--g50);border-bottom:2px solid var(--g200);font-weight:600;color:var(--g600);font-size:11px;text-transform:uppercase;letter-spacing:0.4px;}
.wv-catchup-table td{padding:6px 10px;border-bottom:1px solid var(--g100);vertical-align:top;}
.wv-catchup-table tr:last-child td{border-bottom:none;}
.wv-catchup-notes{margin:0;padding:0 0 0 18px;font-size:12px;color:var(--g600);line-height:1.7;}
/* Full schedule table */
.wv-schedule-scroll{overflow-x:auto;border-radius:var(--radius);box-shadow:var(--shadow);}
.wv-schedule-table{min-width:900px;width:100%;border-collapse:collapse;font-size:12px;background:white;}
.wv-schedule-table th{text-align:center;padding:8px 6px;background:var(--g50);border-bottom:2px solid var(--g200);font-weight:700;color:var(--g600);font-size:11px;position:sticky;top:0;z-index:1;}
.wv-sched-vax-col{text-align:left!important;min-width:200px;position:sticky;left:0;background:var(--g50)!important;z-index:2;}
.wv-sched-age-col{min-width:70px;}
.wv-sched-vax-name{padding:8px 10px;border-bottom:1px solid var(--g100);font-weight:600;color:var(--g700);position:sticky;left:0;background:white;z-index:1;}
.wv-sched-cell{padding:6px 4px;border-bottom:1px solid var(--g100);text-align:center;color:var(--g400);}
.wv-sched-has{background:var(--green-light);color:#065f46;font-weight:700;border-radius:4px;cursor:help;}
.wv-sched-legend{font-size:11px;color:var(--g400);margin-top:10px;display:flex;align-items:center;gap:6px;}
.wv-sched-leg-dot{display:inline-block;width:12px;height:12px;background:var(--green-light);border-radius:2px;}

View file

@ -147,6 +147,10 @@
<i class="fas fa-baby"></i>
<span>Milestones</span>
</button>
<button class="tab-btn" data-tab="wellvisit">
<i class="fas fa-calendar-check"></i>
<span>Well Visit</span>
</button>
<button class="tab-btn hidden" data-tab="admin" id="admin-tab-btn">
<i class="fas fa-user-shield"></i>
<span>Admin</span>
@ -703,6 +707,49 @@
</div>
</section>
<!-- ===== TAB: WELL VISIT / PREVENTIVE CARE ===== -->
<section id="wellvisit-tab" class="tab-content">
<div class="module-header">
<h2><i class="fas fa-calendar-check"></i> Well Visit / Preventive Care</h2>
<p>AAP 2025 Bright Futures periodicity — vaccines, screenings, billing codes, and catch-up schedule</p>
</div>
<!-- Sub-tab navigation -->
<div class="wv-subtab-bar">
<button class="wv-subtab-btn active" data-subtab="byvisit">
<i class="fas fa-child"></i> By Visit Age
</button>
<button class="wv-subtab-btn" data-subtab="schedule">
<i class="fas fa-table"></i> Full Vaccine Schedule
</button>
<button class="wv-subtab-btn" data-subtab="catchup">
<i class="fas fa-rotate"></i> Catch-Up Schedule
</button>
</div>
<!-- By Visit sub-panel -->
<div id="wv-panel-byvisit" class="wv-subpanel">
<div class="wv-visit-selector-row">
<label for="wv-visit-select"><i class="fas fa-calendar-day"></i> Select Visit Age:</label>
<select id="wv-visit-select" class="wv-visit-select"></select>
</div>
<div id="wv-visit-detail" class="wv-visit-detail">
<p class="wv-empty">Select a visit age above to see recommendations.</p>
</div>
</div>
<!-- Full Vaccine Schedule sub-panel -->
<div id="wv-panel-schedule" class="wv-subpanel hidden">
<p class="wv-loading">Loading schedule...</p>
</div>
<!-- Catch-Up Schedule sub-panel -->
<div id="wv-panel-catchup" class="wv-subpanel hidden">
<p class="wv-loading">Loading catch-up schedule...</p>
</div>
</section>
</main>
<!-- SETTINGS MODAL -->
@ -785,6 +832,7 @@
<!-- SCRIPTS -->
<script src="/js/milestonesData.js"></script>
<script src="/js/pediatricScheduleData.js"></script>
<script src="/js/app.js"></script>
<script src="/js/auth.js"></script>
<script src="/js/liveEncounter.js"></script>
@ -794,6 +842,7 @@
<script src="/js/soap.js"></script>
<script src="/js/milestones.js"></script>
<script src="/js/nextcloud.js"></script>
<script src="/js/wellVisit.js"></script>
<script src="/js/admin.js"></script>
</body>
</html>

View file

@ -13,6 +13,7 @@ document.addEventListener('DOMContentLoaded', function() {
var tabId = btn.getAttribute('data-tab') + '-tab';
var tabEl = document.getElementById(tabId);
if (tabEl) tabEl.classList.add('active');
document.dispatchEvent(new CustomEvent('tabChanged', { detail: { tab: btn.getAttribute('data-tab') } }));
});
});

File diff suppressed because it is too large Load diff

371
public/js/wellVisit.js Normal file
View file

@ -0,0 +1,371 @@
// ============================================================
// 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',
};
// ─── 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;
html += '<div class="wv-vax-item">';
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>';
});
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);
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);
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);
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);
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>';
}
panel.innerHTML = html || '<p class="wv-empty">No specific recommendations found for this visit.</p>';
}
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) {
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';
html += '<div class="wv-screen-item">';
html += '<span class="wv-status-badge ' + statusClass + '">' + statusText + '</span>';
html += '<span class="wv-screen-name">' + esc(item.label) + '</span>';
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 () {
// Run when tab becomes visible (lazy init)
var initialized = false;
document.addEventListener('tabChanged', function (e) {
if (e.detail && e.detail.tab === 'wellvisit' && !initialized) {
initialized = true;
init();
}
});
});
})();