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:
parent
1207202f45
commit
2b73db1664
5 changed files with 2218 additions and 0 deletions
|
|
@ -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;}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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') } }));
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
1719
public/js/pediatricScheduleData.js
Normal file
1719
public/js/pediatricScheduleData.js
Normal file
File diff suppressed because it is too large
Load diff
371
public/js/wellVisit.js
Normal file
371
public/js/wellVisit.js
Normal 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 (0–12 mo)',
|
||||
earlyChildhood: 'Early Childhood (1–5 y)',
|
||||
middleChildhood: 'Middle Childhood (6–11 y)',
|
||||
adolescence: 'Adolescence (11–21 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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
// ─── 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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
})();
|
||||
Loading…
Reference in a new issue