feat: Bedside clinical reference module + age→weight estimator + dose-math unit tests
Calculators: - Bedside tab consolidating emergency protocols (Neonatal+Apgar+NRP, Airway/RSI, Cardiac Arrest/PALS, Respiratory, O2 & Ventilation, Status Epilepticus, Sepsis & Fever with PECARN/Aronson/Rochester/Step-by-Step, Anaphylaxis, Procedural Sedation, Agitation, Antiemetics, Antimicrobials, Burns with Lund-Browder body-parts TBSA + Parkland, Toxicology, Trauma). - Global age→weight estimator at top of Calculators tab (APLS + Best Guess). - Pressure-time waveform SVG teaching graphic for Ventilation. - Algorithm image lightbox (fullscreen, Esc/tap-to-close). - Every weight-based dose shows mg/kg inline for clinician verification. - Drug tables wrapped in overflow-x:auto for mobile. Infrastructure: - Pure dose math extracted to public/js/calc-math.js (dual-export Node+browser). - 36 unit tests in test/calc-math.test.js via node:test (zero new deps). - "npm test" added to package.json.
This commit is contained in:
parent
bce3c94f19
commit
2a2787c26d
9 changed files with 2454 additions and 26 deletions
|
|
@ -5,6 +5,7 @@
|
|||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"test": "node --test test/",
|
||||
"maint:check": "node scripts/maintenance.js check",
|
||||
"maint:reindex": "node scripts/maintenance.js reindex",
|
||||
"migrate": "node-pg-migrate",
|
||||
|
|
|
|||
|
|
@ -15,7 +15,52 @@
|
|||
<button class="calc-nav-pill" data-calc="resus">Resus Meds</button>
|
||||
<button class="calc-nav-pill" data-calc="gcs">GCS</button>
|
||||
<button class="calc-nav-pill" data-calc="equipment">Equipment</button>
|
||||
<button class="calc-nav-pill" data-calc="neonatal">Neonatal</button>
|
||||
<button class="calc-nav-pill" data-calc="bedside"><i class="fas fa-briefcase-medical" style="margin-right:5px;color:var(--red);"></i>Bedside</button>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════ GLOBAL AGE → WEIGHT (available for every calculator below) ═══════════ -->
|
||||
<div id="bedside-shared" style="background:var(--g50);border:1px solid var(--g200);border-radius:10px;padding:12px 14px;margin-bottom:20px;">
|
||||
<div style="font-size:11px;font-weight:700;color:var(--g600);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:6px;"><i class="fas fa-weight-scale" style="color:var(--blue);"></i> Age → Weight estimator</div>
|
||||
<div style="display:flex;gap:12px;flex-wrap:wrap;align-items:flex-end;">
|
||||
<div class="calc-field" style="flex:1;min-width:140px;max-width:190px;">
|
||||
<label style="font-weight:600;">Age <span style="font-weight:400;color:var(--g500);font-size:11px;">(e.g. 3y, 6 mo, 15d)</span></label>
|
||||
<input type="text" id="bedside-age" placeholder="e.g. 3y or 6 mo" autocomplete="off">
|
||||
</div>
|
||||
<div class="calc-field" style="flex:1;min-width:150px;max-width:200px;">
|
||||
<label style="font-weight:600;">Formula</label>
|
||||
<select id="bedside-formula">
|
||||
<option value="apls" selected>APLS (Luscombe 2007)</option>
|
||||
<option value="bestguess">Best Guess (Tinning 2007)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="calc-field" style="flex:1;min-width:140px;max-width:190px;">
|
||||
<label style="font-weight:600;">Weight (kg)</label>
|
||||
<input type="number" id="bedside-weight" min="0.3" max="200" step="0.1" placeholder="auto from age, or type">
|
||||
</div>
|
||||
<button id="btn-bedside-clear" class="btn-sm btn-ghost" type="button">Clear</button>
|
||||
</div>
|
||||
<div id="bedside-estimate-note" style="font-size:12px;color:var(--g600);margin-top:8px;min-height:18px;"></div>
|
||||
<div style="font-size:11px;color:var(--g500);margin-top:4px;">Estimator only — copy the weight into whichever calculator you need.</div>
|
||||
|
||||
<!-- Formulas reference (collapsed by default) -->
|
||||
<details style="margin-top:10px;">
|
||||
<summary style="cursor:pointer;font-size:12px;color:var(--blue-dark);font-weight:600;"><i class="fas fa-info-circle"></i> Weight-estimation formulas</summary>
|
||||
<div style="margin-top:8px;padding:10px 12px;background:white;border:1px solid var(--g200);border-radius:6px;font-size:12px;line-height:1.55;">
|
||||
<div style="font-weight:700;color:var(--g700);margin-bottom:2px;">APLS (Luscombe & Owens 2007 / APLS-UK 2016)</div>
|
||||
<div style="padding-left:12px;">
|
||||
• 0-12 months: <strong>(0.5 × age in months) + 4</strong><br>
|
||||
• 1-5 years: <strong>(2 × age in years) + 8</strong><br>
|
||||
• 6-12 years: <strong>(3 × age in years) + 7</strong>
|
||||
</div>
|
||||
<div style="font-weight:700;color:var(--g700);margin:8px 0 2px;">Best Guess (Tinning & Acworth 2007)</div>
|
||||
<div style="padding-left:12px;">
|
||||
• 1-11 months: <strong>(age in months + 9) / 2</strong><br>
|
||||
• 1-4 years: <strong>2 × (age in years + 5)</strong><br>
|
||||
• 5-14 years: <strong>4 × age in years</strong>
|
||||
</div>
|
||||
<div style="margin-top:8px;color:var(--g500);font-style:italic;">Broselow tape (length-based) is most accurate when available.</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════ BP PERCENTILE CALCULATOR ═══════════ -->
|
||||
|
|
@ -638,38 +683,494 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════ NEONATAL ASSESSMENT ═══════════ -->
|
||||
<div id="calc-neonatal" class="calc-panel hidden">
|
||||
<!-- ═══════════ BEDSIDE (consolidated) ═══════════ -->
|
||||
<div id="calc-bedside" class="calc-panel hidden">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-baby"></i> Neonatal Assessment</h3>
|
||||
<span style="font-size:11px;color:var(--g500);">Fenton 2013 / WHO Classification</span>
|
||||
<h3><i class="fas fa-briefcase-medical" style="color:var(--red);"></i> Bedside</h3>
|
||||
<span style="font-size:11px;color:var(--g500);">Weight-based pediatric reference. Always verify against institutional protocols.</span>
|
||||
</div>
|
||||
<div style="padding:16px;">
|
||||
<p style="font-size:12px;color:var(--g500);margin:0 0 14px;">Gestational age classification, birth weight assessment (AGA/SGA/LGA), and prematurity category.</p>
|
||||
<div style="display:flex;gap:16px;flex-wrap:wrap;margin-bottom:16px;">
|
||||
<div class="calc-field" style="flex:1;min-width:140px;">
|
||||
<label>GA (weeks)</label>
|
||||
<input type="number" id="neo-ga-weeks" min="22" max="44" step="1" placeholder="e.g. 34">
|
||||
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:16px;padding-bottom:12px;border-bottom:1px solid var(--g200);">
|
||||
<button class="calc-pill active" data-em="neonatal"><i class="fas fa-baby"></i> Neonatal</button>
|
||||
<button class="calc-pill" data-em="airway"><i class="fas fa-wind"></i> Airway/RSI</button>
|
||||
<button class="calc-pill" data-em="cardiac"><i class="fas fa-heart-pulse"></i> Cardiac Arrest</button>
|
||||
<button class="calc-pill" data-em="respiratory"><i class="fas fa-lungs"></i> Respiratory</button>
|
||||
<button class="calc-pill" data-em="ventilation"><i class="fas fa-fan"></i> O2 & Ventilation</button>
|
||||
<button class="calc-pill" data-em="seizure"><i class="fas fa-brain"></i> Seizures</button>
|
||||
<button class="calc-pill" data-em="sepsis"><i class="fas fa-virus"></i> Sepsis & Fever</button>
|
||||
<button class="calc-pill" data-em="anaphylaxis"><i class="fas fa-syringe"></i> Anaphylaxis</button>
|
||||
<button class="calc-pill" data-em="sedation"><i class="fas fa-bed-pulse"></i> Sedation</button>
|
||||
<button class="calc-pill" data-em="agitation"><i class="fas fa-user-injured"></i> Agitation</button>
|
||||
<button class="calc-pill" data-em="antiemetics"><i class="fas fa-pills"></i> Antiemetics</button>
|
||||
<button class="calc-pill" data-em="antimicrobials"><i class="fas fa-bacterium"></i> Antimicrobials</button>
|
||||
<button class="calc-pill" data-em="burns"><i class="fas fa-fire"></i> Burns</button>
|
||||
<button class="calc-pill" data-em="toxicology"><i class="fas fa-skull-crossbones"></i> Toxicology</button>
|
||||
<button class="calc-pill" data-em="trauma"><i class="fas fa-user-injured" style="transform:rotate(-45deg);"></i> Trauma</button>
|
||||
</div>
|
||||
|
||||
<!-- ── NEONATAL ── -->
|
||||
<div id="em-neonatal" class="em-section">
|
||||
<h4 style="font-size:15px;font-weight:700;color:var(--g800);margin:0 0 4px;">Neonatal Assessment</h4>
|
||||
<p style="font-size:12px;color:var(--g500);margin:0 0 14px;">GA classification, birth weight assessment (AGA/SGA/LGA), and prematurity category. Fenton 2013 / WHO.</p>
|
||||
<div style="display:flex;gap:12px;flex-wrap:wrap;margin-bottom:12px;">
|
||||
<div class="calc-field" style="flex:1;min-width:130px;">
|
||||
<label>GA (weeks)</label>
|
||||
<input type="number" id="neo-ga-weeks" min="22" max="44" step="1" placeholder="e.g. 34">
|
||||
</div>
|
||||
<div class="calc-field" style="flex:1;min-width:130px;">
|
||||
<label>GA (days)</label>
|
||||
<input type="number" id="neo-ga-days" min="0" max="6" step="1" placeholder="0-6">
|
||||
</div>
|
||||
<div class="calc-field" style="flex:1;min-width:130px;">
|
||||
<label>Birth Weight (g)</label>
|
||||
<input type="number" id="neo-weight" min="200" max="6000" step="1" placeholder="e.g. 2500">
|
||||
</div>
|
||||
<div class="calc-field" style="flex:1;min-width:110px;">
|
||||
<label>Sex</label>
|
||||
<select id="neo-sex">
|
||||
<option value="male">Male</option>
|
||||
<option value="female">Female</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="calc-field" style="flex:1;min-width:140px;">
|
||||
<label>GA (days)</label>
|
||||
<input type="number" id="neo-ga-days" min="0" max="6" step="1" placeholder="0-6">
|
||||
</div>
|
||||
<div class="calc-field" style="flex:1;min-width:140px;">
|
||||
<label>Birth Weight (grams)</label>
|
||||
<input type="number" id="neo-weight" min="200" max="6000" step="1" placeholder="e.g. 2500">
|
||||
</div>
|
||||
<div class="calc-field" style="flex:1;min-width:120px;">
|
||||
<label>Sex</label>
|
||||
<select id="neo-sex">
|
||||
<option value="male">Male</option>
|
||||
<option value="female">Female</option>
|
||||
</select>
|
||||
<button id="btn-neo-assess" class="btn-sm btn-primary" style="margin-bottom:12px;"><i class="fas fa-calculator"></i> Assess</button>
|
||||
<div id="neo-result"></div>
|
||||
|
||||
<details style="margin-top:24px;border-top:1px solid var(--g200);padding-top:16px;">
|
||||
<summary style="cursor:pointer;font-size:14px;font-weight:700;color:var(--g800);"><i class="fas fa-heart-pulse" style="color:var(--red);"></i> NRP Resuscitation Pathway</summary>
|
||||
<p style="font-size:12px;color:var(--g500);margin:8px 0;">AHA Neonatal Resuscitation Program 2020. Static visual pathway — enter weight to calculate drug doses.</p>
|
||||
<div style="margin-bottom:10px;">
|
||||
<button class="btn-sm btn-ghost" type="button" data-img-src="/img/nrp_pathway.png" data-img-title="NRP Pathway (reference)"><i class="fas fa-image"></i> View pathway image</button>
|
||||
</div>
|
||||
<div id="nrp-pathway-static"></div>
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:flex-end;margin-top:12px;">
|
||||
<div class="calc-field" style="flex:1;min-width:170px;max-width:240px;">
|
||||
<label>Current Weight (kg)</label>
|
||||
<input type="number" id="nrp-weight" min="0.3" max="6" step="0.01" placeholder="e.g. 3.0">
|
||||
</div>
|
||||
<button id="btn-nrp-calc" class="btn-sm btn-primary"><i class="fas fa-calculator"></i> Calculate NRP Doses</button>
|
||||
</div>
|
||||
<div id="nrp-result" style="margin-top:12px;"></div>
|
||||
</details>
|
||||
|
||||
<!-- Apgar Score -->
|
||||
<div style="margin-top:24px;border-top:1px solid var(--g200);padding-top:16px;">
|
||||
<h4 style="font-size:14px;font-weight:700;color:var(--g800);margin:0 0 4px;"><i class="fas fa-clipboard-check" style="color:var(--blue);"></i> Apgar Score</h4>
|
||||
<p style="font-size:12px;color:var(--g500);margin:0 0 10px;">Assess at <strong>1 and 5 minutes</strong>. If <7 at 5 min, repeat every 5 min up to 20 min. Apgar is a description — <em>never</em> delay resuscitation waiting for a score.</p>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px;">
|
||||
<div class="calc-field" style="flex:1;min-width:150px;">
|
||||
<label><strong>A</strong>ppearance (color)</label>
|
||||
<select id="apgar-appearance">
|
||||
<option value="0">0 — Blue / pale all over</option>
|
||||
<option value="1" selected>1 — Body pink, extremities blue</option>
|
||||
<option value="2">2 — All pink</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="calc-field" style="flex:1;min-width:150px;">
|
||||
<label><strong>P</strong>ulse (HR)</label>
|
||||
<select id="apgar-pulse">
|
||||
<option value="0">0 — Absent</option>
|
||||
<option value="1">1 — <100</option>
|
||||
<option value="2" selected>2 — ≥100</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="calc-field" style="flex:1;min-width:150px;">
|
||||
<label><strong>G</strong>rimace (reflex)</label>
|
||||
<select id="apgar-grimace">
|
||||
<option value="0">0 — No response</option>
|
||||
<option value="1">1 — Grimace / weak cry</option>
|
||||
<option value="2" selected>2 — Cry / active withdrawal</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="calc-field" style="flex:1;min-width:150px;">
|
||||
<label><strong>A</strong>ctivity (tone)</label>
|
||||
<select id="apgar-activity">
|
||||
<option value="0">0 — Limp</option>
|
||||
<option value="1">1 — Some flexion</option>
|
||||
<option value="2" selected>2 — Active motion</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="calc-field" style="flex:1;min-width:150px;">
|
||||
<label><strong>R</strong>espiration</label>
|
||||
<select id="apgar-respiration">
|
||||
<option value="0">0 — Absent</option>
|
||||
<option value="1">1 — Slow / irregular</option>
|
||||
<option value="2" selected>2 — Strong cry</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<button id="btn-apgar-calc" class="btn-sm btn-primary"><i class="fas fa-calculator"></i> Calculate Apgar</button>
|
||||
<div id="apgar-result" style="margin-top:10px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<button id="btn-neo-assess" class="btn-primary" style="margin-bottom:16px;">Assess</button>
|
||||
<div id="neo-result"></div>
|
||||
|
||||
<!-- ── AIRWAY / RSI ── -->
|
||||
<div id="em-airway" class="em-section" style="display:none;">
|
||||
<h4 style="font-size:15px;font-weight:700;color:var(--g800);margin:0 0 4px;">Airway Management / RSI</h4>
|
||||
<p style="font-size:12px;color:var(--g500);margin:0 0 14px;">RSI drugs + equipment sizing + initial ventilator settings.</p>
|
||||
<div style="display:flex;gap:12px;flex-wrap:wrap;margin-bottom:12px;">
|
||||
<div class="calc-field" style="flex:1;min-width:140px;max-width:200px;">
|
||||
<label>Weight (kg)</label>
|
||||
<input type="number" id="airway-weight" min="1" max="150" step="0.1" placeholder="e.g. 20">
|
||||
</div>
|
||||
<div class="calc-field" style="flex:1;min-width:140px;max-width:200px;">
|
||||
<label>Age (years, for equipment sizing)</label>
|
||||
<input type="number" id="airway-age" min="0" max="18" step="0.5" placeholder="e.g. 5">
|
||||
</div>
|
||||
</div>
|
||||
<button id="btn-airway-calc" class="btn-sm btn-primary" style="margin-bottom:12px;"><i class="fas fa-calculator"></i> Calculate</button>
|
||||
<div id="airway-result"></div>
|
||||
</div>
|
||||
|
||||
<!-- ── CARDIAC ARREST / PALS ── -->
|
||||
<div id="em-cardiac" class="em-section" style="display:none;">
|
||||
<h4 style="font-size:15px;font-weight:700;color:var(--g800);margin:0 0 4px;">Cardiac Arrest / PALS</h4>
|
||||
<p style="font-size:12px;color:var(--g500);margin:0 0 14px;">PALS algorithm medications for arrest and peri-arrest rhythms.</p>
|
||||
<div class="calc-field" style="max-width:200px;margin-bottom:12px;">
|
||||
<label>Weight (kg)</label>
|
||||
<input type="number" id="cardiac-weight" min="1" max="150" step="0.1" placeholder="e.g. 20">
|
||||
</div>
|
||||
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px;">
|
||||
<button class="btn-sm btn-primary" data-cardiac="general">General Doses</button>
|
||||
<button class="btn-sm btn-primary" data-cardiac="asystole">Asystole / PEA</button>
|
||||
<button class="btn-sm btn-primary" data-cardiac="brady">Bradycardia</button>
|
||||
<button class="btn-sm btn-primary" data-cardiac="svt">SVT</button>
|
||||
<button class="btn-sm btn-primary" data-cardiac="vfib">VF / Pulseless VT</button>
|
||||
<button class="btn-sm btn-primary" data-cardiac="vt">Stable VT</button>
|
||||
</div>
|
||||
<div id="cardiac-result"></div>
|
||||
</div>
|
||||
|
||||
<!-- ── RESPIRATORY ── -->
|
||||
<div id="em-respiratory" class="em-section" style="display:none;">
|
||||
<h4 style="font-size:15px;font-weight:700;color:var(--g800);margin:0 0 4px;">Respiratory Management</h4>
|
||||
<p style="font-size:12px;color:var(--g500);margin:0 0 14px;">Asthma, croup, bronchiolitis — scores, stepwise management, weight-based drug dosing.</p>
|
||||
<div class="calc-field" style="max-width:200px;margin-bottom:12px;">
|
||||
<label>Weight (kg)</label>
|
||||
<input type="number" id="resp-weight" min="1" max="150" step="0.1" placeholder="e.g. 15">
|
||||
</div>
|
||||
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:14px;">
|
||||
<button class="calc-pill active" data-resp="asthma">Asthma</button>
|
||||
<button class="calc-pill" data-resp="croup">Croup</button>
|
||||
<button class="calc-pill" data-resp="bronchiolitis">Bronchiolitis</button>
|
||||
</div>
|
||||
|
||||
<div id="resp-asthma">
|
||||
<p style="font-size:12px;color:var(--g500);margin:0 0 10px;">Select severity to see stepwise management with weight-based dosing.</p>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:14px;">
|
||||
<button class="btn-sm" data-asthma-severity="mild" style="background:var(--green);color:white;">Mild</button>
|
||||
<button class="btn-sm" data-asthma-severity="moderate" style="background:var(--amber);color:white;">Moderate</button>
|
||||
<button class="btn-sm" data-asthma-severity="severe" style="background:var(--red);color:white;">Severe</button>
|
||||
</div>
|
||||
<div id="asthma-result"></div>
|
||||
<div style="margin-top:16px;border-top:1px solid var(--g200);padding-top:12px;">
|
||||
<label style="font-size:12px;font-weight:600;color:var(--g600);margin-bottom:8px;display:block;">PRAM Score (Pediatric Respiratory Assessment Measure)</label>
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;margin-bottom:10px;">
|
||||
<div class="calc-field" style="flex:1;min-width:160px;">
|
||||
<label>O2 Saturation</label>
|
||||
<select id="pram-spo2"><option value="0">≥95%</option><option value="1">92-94%</option><option value="2"><92%</option></select>
|
||||
</div>
|
||||
<div class="calc-field" style="flex:1;min-width:160px;">
|
||||
<label>Suprasternal Retractions</label>
|
||||
<select id="pram-retractions"><option value="0">Absent</option><option value="2">Present</option></select>
|
||||
</div>
|
||||
<div class="calc-field" style="flex:1;min-width:160px;">
|
||||
<label>Scalene Contraction</label>
|
||||
<select id="pram-scalene"><option value="0">Absent</option><option value="2">Present</option></select>
|
||||
</div>
|
||||
<div class="calc-field" style="flex:1;min-width:160px;">
|
||||
<label>Air Entry</label>
|
||||
<select id="pram-air"><option value="0">Normal</option><option value="1">Decreased at bases</option><option value="2">Widespread decrease</option><option value="3">Absent/minimal</option></select>
|
||||
</div>
|
||||
<div class="calc-field" style="flex:1;min-width:160px;">
|
||||
<label>Wheezing</label>
|
||||
<select id="pram-wheeze"><option value="0">Absent</option><option value="1">Expiratory only</option><option value="2">Insp & exp</option><option value="3">Audible / silent chest</option></select>
|
||||
</div>
|
||||
</div>
|
||||
<button id="btn-pram-calc" class="btn-sm btn-primary"><i class="fas fa-calculator"></i> Calculate PRAM</button>
|
||||
<div id="pram-result" style="margin-top:8px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="resp-croup" style="display:none;">
|
||||
<p style="font-size:12px;color:var(--g500);margin:0 0 10px;">Westley Croup Score → severity-based treatment.</p>
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;margin-bottom:10px;">
|
||||
<div class="calc-field" style="flex:1;min-width:160px;">
|
||||
<label>Consciousness</label>
|
||||
<select id="croup-conscious"><option value="0">Normal</option><option value="5">Disoriented</option></select>
|
||||
</div>
|
||||
<div class="calc-field" style="flex:1;min-width:160px;">
|
||||
<label>Cyanosis</label>
|
||||
<select id="croup-cyanosis"><option value="0">None</option><option value="4">With agitation</option><option value="5">At rest</option></select>
|
||||
</div>
|
||||
<div class="calc-field" style="flex:1;min-width:160px;">
|
||||
<label>Stridor</label>
|
||||
<select id="croup-stridor"><option value="0">None</option><option value="1">With agitation</option><option value="2">At rest</option></select>
|
||||
</div>
|
||||
<div class="calc-field" style="flex:1;min-width:160px;">
|
||||
<label>Air Entry</label>
|
||||
<select id="croup-air"><option value="0">Normal</option><option value="1">Decreased</option><option value="2">Markedly decreased</option></select>
|
||||
</div>
|
||||
<div class="calc-field" style="flex:1;min-width:160px;">
|
||||
<label>Retractions</label>
|
||||
<select id="croup-retractions"><option value="0">None</option><option value="1">Mild</option><option value="2">Moderate</option><option value="3">Severe</option></select>
|
||||
</div>
|
||||
</div>
|
||||
<button id="btn-croup-calc" class="btn-sm btn-primary"><i class="fas fa-calculator"></i> Calculate Westley Score</button>
|
||||
<div id="croup-result" style="margin-top:10px;"></div>
|
||||
</div>
|
||||
|
||||
<div id="resp-bronchiolitis" style="display:none;">
|
||||
<p style="font-size:12px;color:var(--g500);margin:0 0 10px;">Bronchiolitis management per AAP 2014/2023 — primarily supportive.</p>
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;margin-bottom:10px;">
|
||||
<div class="calc-field" style="flex:1;min-width:160px;">
|
||||
<label>Age</label>
|
||||
<select id="bronch-age"><option value="<12w"><12 weeks</option><option value="12w-12m">12 wks - 12 mo</option><option value=">12m">>12 months</option></select>
|
||||
</div>
|
||||
<div class="calc-field" style="flex:1;min-width:160px;">
|
||||
<label>O2 Saturation</label>
|
||||
<select id="bronch-spo2"><option value="normal">≥90%</option><option value="low"><90%</option></select>
|
||||
</div>
|
||||
<div class="calc-field" style="flex:1;min-width:160px;">
|
||||
<label>Hydration</label>
|
||||
<select id="bronch-hydration"><option value="adequate">Adequate</option><option value="poor">Poor / dehydrated</option></select>
|
||||
</div>
|
||||
<div class="calc-field" style="flex:1;min-width:160px;">
|
||||
<label>Resp Distress</label>
|
||||
<select id="bronch-distress"><option value="mild">Mild</option><option value="moderate">Moderate</option><option value="severe">Severe / apnea</option></select>
|
||||
</div>
|
||||
</div>
|
||||
<button id="btn-bronch-calc" class="btn-sm btn-primary"><i class="fas fa-calculator"></i> Assess</button>
|
||||
<div id="bronch-result" style="margin-top:10px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── O2 & VENTILATION (teaching + reference) ── -->
|
||||
<div id="em-ventilation" class="em-section" style="display:none;">
|
||||
<h4 style="font-size:15px;font-weight:700;color:var(--g800);margin:0 0 14px;">Oxygen & Ventilation</h4>
|
||||
<div style="display:flex;gap:12px;flex-wrap:wrap;margin-bottom:12px;">
|
||||
<div class="calc-field" style="flex:1;min-width:140px;max-width:200px;">
|
||||
<label>Weight (kg)</label>
|
||||
<input type="number" id="vent-weight" min="1" max="150" step="0.1" placeholder="e.g. 20">
|
||||
</div>
|
||||
<div class="calc-field" style="flex:1;min-width:140px;max-width:200px;">
|
||||
<label>Age (years, optional)</label>
|
||||
<input type="number" id="vent-age" min="0" max="18" step="0.5" placeholder="e.g. 5">
|
||||
</div>
|
||||
</div>
|
||||
<button id="btn-vent-show" class="btn-sm btn-primary" style="margin-bottom:12px;"><i class="fas fa-diagram-project"></i> Show Reference</button>
|
||||
<div id="vent-result"></div>
|
||||
</div>
|
||||
|
||||
<!-- ── SEIZURES / STATUS EPILEPTICUS ── -->
|
||||
<div id="em-seizure" class="em-section" style="display:none;">
|
||||
<h4 style="font-size:15px;font-weight:700;color:var(--g800);margin:0 0 4px;">Status Epilepticus</h4>
|
||||
<p style="font-size:12px;color:var(--g500);margin:0 0 14px;">Time-based pathway — stabilize → 1st benzo → 2nd benzo → 2nd-line AED → refractory.</p>
|
||||
<div class="calc-field" style="max-width:200px;margin-bottom:12px;">
|
||||
<label>Weight (kg)</label>
|
||||
<input type="number" id="seizure-weight" min="1" max="150" step="0.1" placeholder="e.g. 20">
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px;">
|
||||
<button id="btn-seizure-calc" class="btn-sm btn-primary"><i class="fas fa-diagram-project"></i> Show Pathway</button>
|
||||
<button class="btn-sm btn-ghost" data-img-src="/img/epilepsy_eiic_pathway.png" data-img-title="Pediatric Status Epilepticus Pathway (reference)"><i class="fas fa-image"></i> View pathway image</button>
|
||||
</div>
|
||||
<div id="seizure-result"></div>
|
||||
</div>
|
||||
|
||||
<!-- ── SEPSIS ── -->
|
||||
<div id="em-sepsis" class="em-section" style="display:none;">
|
||||
<h4 style="font-size:15px;font-weight:700;color:var(--g800);margin:0 0 4px;">Sepsis & Fever Workup</h4>
|
||||
<p style="font-size:12px;color:var(--g500);margin:0 0 14px;">Phoenix Sepsis Criteria (2024), age-based approach, first-hour bundle, and validated febrile-infant rules (PECARN / Aronson / Rochester / Step-by-Step).</p>
|
||||
<div style="display:flex;gap:12px;flex-wrap:wrap;margin-bottom:12px;">
|
||||
<div class="calc-field" style="flex:1;min-width:140px;max-width:200px;">
|
||||
<label>Weight (kg)</label>
|
||||
<input type="number" id="sepsis-weight" min="1" max="150" step="0.1" placeholder="e.g. 20">
|
||||
</div>
|
||||
<div class="calc-field" style="flex:1;min-width:180px;max-width:240px;">
|
||||
<label>Age group</label>
|
||||
<select id="sepsis-age">
|
||||
<option value="neonate">Neonate (0-28 days)</option>
|
||||
<option value="infant">Young infant (29 d - 3 mo)</option>
|
||||
<option value="child" selected>Older child / adolescent (>3 mo)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<button id="btn-sepsis-show" class="btn-sm btn-primary" style="margin-bottom:12px;"><i class="fas fa-list-check"></i> Show Approach</button>
|
||||
<div id="sepsis-result"></div>
|
||||
</div>
|
||||
|
||||
<!-- ── ANAPHYLAXIS ── -->
|
||||
<div id="em-anaphylaxis" class="em-section" style="display:none;">
|
||||
<h4 style="font-size:15px;font-weight:700;color:var(--g800);margin:0 0 4px;">Anaphylaxis</h4>
|
||||
<p style="font-size:12px;color:var(--g500);margin:0 0 14px;">Stepwise management with weight-based dosing.</p>
|
||||
<div style="display:flex;gap:12px;flex-wrap:wrap;margin-bottom:12px;">
|
||||
<div class="calc-field" style="flex:1;min-width:140px;max-width:200px;">
|
||||
<label>Weight (kg)</label>
|
||||
<input type="number" id="anaph-weight" min="1" max="150" step="0.1" placeholder="e.g. 25">
|
||||
</div>
|
||||
<div class="calc-field" style="flex:1;min-width:140px;max-width:200px;">
|
||||
<label>Age Group</label>
|
||||
<select id="anaph-age"><option value="infant"><1 year</option><option value="child" selected>1-12 years</option><option value="adolescent">>12 years</option></select>
|
||||
</div>
|
||||
</div>
|
||||
<button id="btn-anaph-calc" class="btn-sm btn-primary" style="margin-bottom:12px;"><i class="fas fa-calculator"></i> Calculate Doses</button>
|
||||
<div id="anaph-result"></div>
|
||||
</div>
|
||||
|
||||
<!-- ── PROCEDURAL SEDATION ── -->
|
||||
<div id="em-sedation" class="em-section" style="display:none;">
|
||||
<h4 style="font-size:15px;font-weight:700;color:var(--g800);margin:0 0 4px;">Procedural Sedation</h4>
|
||||
<p style="font-size:12px;color:var(--g500);margin:0 0 14px;">Sedation agents + reversal drugs.</p>
|
||||
<div class="calc-field" style="max-width:200px;margin-bottom:12px;">
|
||||
<label>Weight (kg)</label>
|
||||
<input type="number" id="sed-weight" min="1" max="150" step="0.1" placeholder="e.g. 15">
|
||||
</div>
|
||||
<button id="btn-sed-calc" class="btn-sm btn-primary" style="margin-bottom:12px;"><i class="fas fa-calculator"></i> Calculate Doses</button>
|
||||
<div id="sed-result"></div>
|
||||
</div>
|
||||
|
||||
<!-- ── AGITATION ── -->
|
||||
<div id="em-agitation" class="em-section" style="display:none;">
|
||||
<h4 style="font-size:15px;font-weight:700;color:var(--g800);margin:0 0 4px;">Acute Agitation</h4>
|
||||
<p style="font-size:12px;color:var(--g500);margin:0 0 14px;">Stepwise: verbal de-escalation → oral → IM/IV.</p>
|
||||
<div class="calc-field" style="max-width:200px;margin-bottom:12px;">
|
||||
<label>Weight (kg)</label>
|
||||
<input type="number" id="agit-weight" min="1" max="150" step="0.1" placeholder="e.g. 30">
|
||||
</div>
|
||||
<button id="btn-agit-calc" class="btn-sm btn-primary" style="margin-bottom:12px;"><i class="fas fa-calculator"></i> Calculate Doses</button>
|
||||
<div id="agit-result"></div>
|
||||
</div>
|
||||
|
||||
<!-- ── ANTIEMETICS ── -->
|
||||
<div id="em-antiemetics" class="em-section" style="display:none;">
|
||||
<h4 style="font-size:15px;font-weight:700;color:var(--g800);margin:0 0 4px;">Antiemetics</h4>
|
||||
<p style="font-size:12px;color:var(--g500);margin:0 0 14px;">Common pediatric antiemetic dosing with max doses and key cautions.</p>
|
||||
<div class="calc-field" style="max-width:200px;margin-bottom:12px;">
|
||||
<label>Weight (kg)</label>
|
||||
<input type="number" id="emet-weight" min="1" max="150" step="0.1" placeholder="e.g. 20">
|
||||
</div>
|
||||
<button id="btn-emet-calc" class="btn-sm btn-primary" style="margin-bottom:12px;"><i class="fas fa-calculator"></i> Calculate Doses</button>
|
||||
<div id="emet-result"></div>
|
||||
</div>
|
||||
|
||||
<!-- ── ANTIMICROBIALS ── -->
|
||||
<div id="em-antimicrobials" class="em-section" style="display:none;">
|
||||
<h4 style="font-size:15px;font-weight:700;color:var(--g800);margin:0 0 4px;">Empiric Antimicrobials</h4>
|
||||
<p style="font-size:12px;color:var(--g500);margin:0 0 14px;">Empiric regimens by age and infection type. Always tailor to local resistance, cultures, and severity.</p>
|
||||
<div style="display:flex;gap:12px;flex-wrap:wrap;margin-bottom:12px;">
|
||||
<div class="calc-field" style="flex:1;min-width:160px;">
|
||||
<label>Age</label>
|
||||
<select id="abx-age">
|
||||
<option value="neonate">Neonate (0-28 days)</option>
|
||||
<option value="infant">Infant (1-3 months)</option>
|
||||
<option value="child">Child (>3 months)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="calc-field" style="flex:1;min-width:200px;">
|
||||
<label>Infection</label>
|
||||
<select id="abx-infection">
|
||||
<option value="sepsis">Sepsis / bacteremia</option>
|
||||
<option value="meningitis">Meningitis</option>
|
||||
<option value="pna">Community-acquired pneumonia</option>
|
||||
<option value="uti">UTI / pyelonephritis</option>
|
||||
<option value="skin">Skin / soft tissue</option>
|
||||
<option value="ent">AOM / sinusitis / pharyngitis</option>
|
||||
<option value="neutropenic">Febrile neutropenia</option>
|
||||
<option value="ic">Intra-abdominal</option>
|
||||
<option value="bone">Osteomyelitis / septic arthritis</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<button id="btn-abx-lookup" class="btn-sm btn-primary" style="margin-bottom:12px;"><i class="fas fa-search"></i> Look Up Regimen</button>
|
||||
<div id="abx-result"></div>
|
||||
</div>
|
||||
|
||||
<!-- ── BURNS ── -->
|
||||
<div id="em-burns" class="em-section" style="display:none;">
|
||||
<h4 style="font-size:15px;font-weight:700;color:var(--g800);margin:0 0 4px;">Burn Management</h4>
|
||||
<p style="font-size:12px;color:var(--g500);margin:0 0 14px;">TBSA body-parts calculator (Lund-Browder age-adjusted) + Parkland fluid resuscitation. Count only 2nd-degree or deeper.</p>
|
||||
<div style="display:flex;gap:12px;flex-wrap:wrap;margin-bottom:12px;">
|
||||
<div class="calc-field" style="flex:1;min-width:130px;max-width:180px;">
|
||||
<label>Weight (kg)</label>
|
||||
<input type="number" id="burn-weight" min="1" max="150" step="0.1" placeholder="e.g. 20">
|
||||
</div>
|
||||
<div class="calc-field" style="flex:1;min-width:130px;max-width:180px;">
|
||||
<label>Age group</label>
|
||||
<select id="burn-age">
|
||||
<option value="infant">Infant (<1 yr)</option>
|
||||
<option value="young" selected>1-5 years</option>
|
||||
<option value="child">5-10 years</option>
|
||||
<option value="adol">10-15 years</option>
|
||||
<option value="adult">>15 / adult</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="calc-field" style="flex:1;min-width:130px;max-width:180px;">
|
||||
<label>Override TBSA %</label>
|
||||
<input type="number" id="burn-tbsa" min="0" max="100" step="0.5" placeholder="auto from body parts">
|
||||
</div>
|
||||
</div>
|
||||
<div id="burn-bodyparts-wrapper" style="margin-bottom:12px;"></div>
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-bottom:12px;">
|
||||
<button id="btn-burn-calc" class="btn-sm btn-primary" type="button"><i class="fas fa-calculator"></i> Calculate Parkland</button>
|
||||
<button id="btn-burn-reset" class="btn-sm btn-ghost" type="button">Reset body parts</button>
|
||||
<span id="burn-tbsa-live" style="font-size:13px;font-weight:700;color:var(--blue-dark);"></span>
|
||||
</div>
|
||||
<div id="burn-result"></div>
|
||||
</div>
|
||||
|
||||
<!-- ── TOXICOLOGY ── -->
|
||||
<div id="em-toxicology" class="em-section" style="display:none;">
|
||||
<h4 style="font-size:15px;font-weight:700;color:var(--g800);margin:0 0 4px;">Toxicology</h4>
|
||||
<p style="font-size:12px;color:var(--g500);margin:0 0 14px;">Approach, decontamination, common ingestions with antidotes, dialyzable drugs reference.</p>
|
||||
<div style="display:flex;gap:12px;flex-wrap:wrap;margin-bottom:12px;">
|
||||
<div class="calc-field" style="flex:1;min-width:140px;max-width:200px;">
|
||||
<label>Weight (kg)</label>
|
||||
<input type="number" id="tox-weight" min="1" max="150" step="0.1" placeholder="e.g. 20">
|
||||
</div>
|
||||
<div class="calc-field" style="flex:1;min-width:200px;">
|
||||
<label>Topic</label>
|
||||
<select id="tox-topic">
|
||||
<option value="approach">General approach + decontamination</option>
|
||||
<option value="acetaminophen">Acetaminophen</option>
|
||||
<option value="opioids">Opioids</option>
|
||||
<option value="iron">Iron</option>
|
||||
<option value="tca">TCAs</option>
|
||||
<option value="bbccb">β-blocker / CCB</option>
|
||||
<option value="benzo">Benzodiazepines</option>
|
||||
<option value="organo">Organophosphates / carbamates</option>
|
||||
<option value="salicylate">Salicylates</option>
|
||||
<option value="etoh">Toxic alcohols</option>
|
||||
<option value="dialyzable">Dialyzable drugs (ISTUMBLE)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<button id="btn-tox-lookup" class="btn-sm btn-primary" style="margin-bottom:12px;"><i class="fas fa-search"></i> Look Up</button>
|
||||
<div id="tox-result"></div>
|
||||
</div>
|
||||
|
||||
<!-- ── TRAUMA ── -->
|
||||
<div id="em-trauma" class="em-section" style="display:none;">
|
||||
<h4 style="font-size:15px;font-weight:700;color:var(--g800);margin:0 0 4px;">Trauma</h4>
|
||||
<p style="font-size:12px;color:var(--g500);margin:0 0 14px;">Primary / secondary survey, c-spine, massive transfusion, TXA.</p>
|
||||
<div class="calc-field" style="max-width:200px;margin-bottom:12px;">
|
||||
<label>Weight (kg)</label>
|
||||
<input type="number" id="trauma-weight" min="1" max="150" step="0.1" placeholder="e.g. 30">
|
||||
</div>
|
||||
<button id="btn-trauma-show" class="btn-sm btn-primary" style="margin-bottom:12px;"><i class="fas fa-list"></i> Show Reference</button>
|
||||
<div id="trauma-result"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════ IMAGE LIGHTBOX (reusable, any section can trigger via [data-img-src]) ═══════════ -->
|
||||
<div id="img-lightbox" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.85);z-index:9999;align-items:center;justify-content:center;padding:20px;flex-direction:column;">
|
||||
<div style="display:flex;align-items:center;gap:12px;margin-bottom:10px;color:white;max-width:95%;">
|
||||
<div id="img-lightbox-title" style="flex:1;font-weight:600;font-size:14px;"></div>
|
||||
<a id="img-lightbox-open" href="#" target="_blank" rel="noopener" style="color:#93c5fd;font-size:13px;text-decoration:none;"><i class="fas fa-up-right-from-square"></i> Open in new tab</a>
|
||||
<button id="img-lightbox-close" type="button" style="background:rgba(255,255,255,0.15);color:white;border:none;border-radius:6px;padding:6px 10px;cursor:pointer;font-size:14px;"><i class="fas fa-xmark"></i> Close</button>
|
||||
</div>
|
||||
<img id="img-lightbox-img" src="" alt="" style="max-width:95%;max-height:85vh;object-fit:contain;border-radius:6px;box-shadow:0 4px 30px rgba(0,0,0,0.5);background:white;">
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -143,6 +143,7 @@ body{font-family:'Inter',system-ui,sans-serif;background:var(--g50);color:var(--
|
|||
.btn-generate-green{background:var(--green);box-shadow:0 3px 10px rgba(16,185,129,0.25);}
|
||||
|
||||
.btn-sm{display:inline-flex;align-items:center;gap:4px;padding:5px 10px;border:none;border-radius:6px;font-size:12px;font-weight:500;cursor:pointer;font-family:inherit;transition:all 0.15s;}
|
||||
.btn-lg{display:inline-flex;align-items:center;gap:6px;padding:9px 18px;border:none;border-radius:8px;font-size:14px;font-weight:600;cursor:pointer;font-family:inherit;transition:all 0.15s;}
|
||||
.btn-primary{background:var(--blue);color:white;}.btn-primary:hover{background:var(--blue-dark);}
|
||||
.btn-ghost{background:var(--g200);color:var(--g700);}.btn-ghost:hover{background:var(--g300);}
|
||||
.btn-success{background:var(--green);color:white;}.btn-success:hover{background:#059669;}
|
||||
|
|
|
|||
BIN
public/img/epilepsy_eiic_pathway.png
Normal file
BIN
public/img/epilepsy_eiic_pathway.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 326 KiB |
BIN
public/img/nrp_pathway.png
Normal file
BIN
public/img/nrp_pathway.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 144 KiB |
|
|
@ -444,6 +444,7 @@
|
|||
<script defer src="/js/encounters.js"></script>
|
||||
<script defer src="/js/memories.js"></script>
|
||||
<script defer src="/js/documents.js"></script>
|
||||
<script defer src="/js/calc-math.js"></script>
|
||||
<script defer src="/js/calculators.js"></script>
|
||||
<script defer src="/js/learningHub.js"></script>
|
||||
<script defer src="/js/admin.js"></script>
|
||||
|
|
|
|||
206
public/js/calc-math.js
Normal file
206
public/js/calc-math.js
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
// ============================================================
|
||||
// calc-math.js — Pure, side-effect-free dose / score math.
|
||||
// Dual-exported: browser (window._PED_MATH) + Node (module.exports).
|
||||
// Keep this file free of DOM references so it can be unit-tested
|
||||
// under Node with `node --test test/`.
|
||||
// ============================================================
|
||||
(function() {
|
||||
function roundTo(v, places) { var m = Math.pow(10, places); return Math.round(v * m) / m; }
|
||||
|
||||
// ── Age → Weight ────────────────────────────────────────────
|
||||
// APLS (Luscombe & Owens 2007 / APLS-UK 2016) and Best Guess
|
||||
// (Tinning & Acworth 2007). For ages ≥13 yr we use Best Guess
|
||||
// as primary; otherwise APLS.
|
||||
function estimateWeightFromAgeMonths(months) {
|
||||
if (months == null || isNaN(months) || months < 0) return null;
|
||||
var years = months / 12;
|
||||
var apls;
|
||||
if (months < 12) apls = 0.5 * months + 4;
|
||||
else if (years <= 5) apls = 2 * years + 8;
|
||||
else apls = 3 * years + 7;
|
||||
var bg;
|
||||
if (months < 12) bg = (months + 9) / 2;
|
||||
else if (years <= 5) bg = 2 * (years + 5);
|
||||
else bg = 4 * years;
|
||||
var primary = years < 13 ? apls : bg;
|
||||
var bandLabel;
|
||||
if (years < 13) {
|
||||
if (months < 12) bandLabel = 'APLS 0-12 mo';
|
||||
else if (years <= 5) bandLabel = 'APLS 1-5 yr';
|
||||
else bandLabel = 'APLS 6-12 yr';
|
||||
} else {
|
||||
bandLabel = 'Best Guess 5-14 yr';
|
||||
}
|
||||
return {
|
||||
weight: Math.max(0.3, roundTo(primary, 1)),
|
||||
formulaLabel: bandLabel,
|
||||
all: { apls: roundTo(apls, 1), bestGuess: roundTo(bg, 1) }
|
||||
};
|
||||
}
|
||||
|
||||
// ── Parkland fluid resuscitation (burn) ─────────────────────
|
||||
// 4 mL × weight(kg) × %TBSA over 24 h; half in first 8 h.
|
||||
function parkland(weightKg, tbsaPct) {
|
||||
if (!weightKg || weightKg <= 0 || !tbsaPct || tbsaPct <= 0) return null;
|
||||
var total = 4 * weightKg * tbsaPct;
|
||||
var first8 = total / 2;
|
||||
var next16 = total - first8;
|
||||
return {
|
||||
totalMl: Math.round(total),
|
||||
first8Ml: Math.round(first8),
|
||||
next16Ml: Math.round(next16),
|
||||
rateFirst8: Math.round(first8 / 8),
|
||||
rateNext16: Math.round(next16 / 16)
|
||||
};
|
||||
}
|
||||
|
||||
// ── Maintenance fluids (Holliday-Segar 4-2-1 rule) ─────────
|
||||
function maintenance421(weightKg) {
|
||||
if (!weightKg || weightKg <= 0) return null;
|
||||
if (weightKg <= 10) return Math.round(weightKg * 4);
|
||||
if (weightKg <= 20) return Math.round(40 + (weightKg - 10) * 2);
|
||||
return Math.round(60 + (weightKg - 20));
|
||||
}
|
||||
|
||||
// ── PRAM score (asthma, 0-12) ───────────────────────────────
|
||||
function pramScore(spo2, retractions, scalene, airEntry, wheeze) {
|
||||
var total = [spo2, retractions, scalene, airEntry, wheeze]
|
||||
.reduce(function(s, v) { return s + (parseInt(v, 10) || 0); }, 0);
|
||||
var severity = total <= 3 ? 'Mild' : total <= 7 ? 'Moderate' : 'Severe';
|
||||
return { total: total, severity: severity };
|
||||
}
|
||||
|
||||
// ── Westley croup score (0-17) ──────────────────────────────
|
||||
function westleyScore(conscious, cyanosis, stridor, airEntry, retractions) {
|
||||
var total = [conscious, cyanosis, stridor, airEntry, retractions]
|
||||
.reduce(function(s, v) { return s + (parseInt(v, 10) || 0); }, 0);
|
||||
var severity;
|
||||
if (total <= 2) severity = 'Mild';
|
||||
else if (total <= 5) severity = 'Moderate';
|
||||
else if (total <= 11) severity = 'Severe';
|
||||
else severity = 'Impending respiratory failure';
|
||||
return { total: total, severity: severity };
|
||||
}
|
||||
|
||||
// ── Epinephrine: anaphylaxis IM vs arrest IV ────────────────
|
||||
// IMPORTANT: different concentrations (1:1,000 vs 1:10,000).
|
||||
function epiAnaphylaxisIM(weightKg) {
|
||||
if (!weightKg || weightKg <= 0) return null;
|
||||
var mg = Math.min(weightKg * 0.01, 0.5);
|
||||
return { doseMg: roundTo(mg, 2), volumeMl_1in1000: roundTo(mg, 2) };
|
||||
}
|
||||
function epiArrestIV(weightKg) {
|
||||
if (!weightKg || weightKg <= 0) return null;
|
||||
var mg = Math.min(weightKg * 0.01, 1);
|
||||
return { doseMg: roundTo(mg, 2), volumeMl_1in10000: roundTo(mg * 10, 2) };
|
||||
}
|
||||
|
||||
// ── NRP epinephrine (neonatal resus) ────────────────────────
|
||||
// IV/IO 0.01-0.03 mg/kg = 0.1-0.3 mL/kg of 1:10,000.
|
||||
function nrpEpiIV(weightKg) {
|
||||
if (!weightKg || weightKg <= 0) return null;
|
||||
return {
|
||||
lowMg: roundTo(weightKg * 0.01, 2),
|
||||
highMg: roundTo(weightKg * 0.03, 2),
|
||||
lowMl: roundTo(weightKg * 0.1, 2),
|
||||
highMl: roundTo(weightKg * 0.3, 2)
|
||||
};
|
||||
}
|
||||
|
||||
// ── RSI drug doses (selected) ───────────────────────────────
|
||||
function rsiKetamine(weightKg) {
|
||||
if (!weightKg) return null;
|
||||
return { lowMg: Math.min(roundTo(weightKg * 1.5, 1), 150), highMg: Math.min(roundTo(weightKg * 2, 1), 200) };
|
||||
}
|
||||
function rsiRocuronium(weightKg) {
|
||||
if (!weightKg) return null;
|
||||
return { mg: Math.min(roundTo(weightKg * 1.2, 1), 100) };
|
||||
}
|
||||
function rsiSuccinylcholine(weightKg) {
|
||||
if (!weightKg) return null;
|
||||
var perKg = weightKg < 10 ? 2 : 1.5;
|
||||
return { mg: Math.min(roundTo(weightKg * perKg, 1), 150), perKg: perKg };
|
||||
}
|
||||
|
||||
// ── Minimum acceptable systolic BP (1-10 yr) ────────────────
|
||||
function minSBP(years) {
|
||||
if (years == null || isNaN(years)) return null;
|
||||
if (years < 1) return 70;
|
||||
if (years > 10) return 90;
|
||||
return 70 + 2 * years;
|
||||
}
|
||||
|
||||
// ── ETT (endotracheal tube) sizing by age ───────────────────
|
||||
function ettSize(ageYears) {
|
||||
if (ageYears == null || ageYears < 0) return null;
|
||||
var uncuffed = ageYears / 4 + 4;
|
||||
var cuffed = ageYears / 4 + 3.5;
|
||||
return {
|
||||
uncuffedMm: roundTo(uncuffed, 1),
|
||||
cuffedMm: roundTo(cuffed, 1),
|
||||
depthCm: roundTo(uncuffed * 3, 1)
|
||||
};
|
||||
}
|
||||
|
||||
// ── Lund-Browder regional TBSA by age bracket ───────────────
|
||||
// Used for burns TBSA calculation. Bracket: 'infant', 'young',
|
||||
// 'child', 'adol', 'adult' — per 0-1, 1-5, 5-10, 10-15, >15 yr.
|
||||
var LB_BRACKETS = ['infant', 'young', 'child', 'adol', 'adult'];
|
||||
var LB = {
|
||||
head: [18, 13, 11, 9, 7],
|
||||
neck: [2, 2, 2, 2, 2],
|
||||
ant_trunk: [13, 13, 13, 13, 13],
|
||||
post_trunk: [13, 13, 13, 13, 13],
|
||||
r_buttock: [2.5, 2.5, 2.5, 2.5, 2.5],
|
||||
l_buttock: [2.5, 2.5, 2.5, 2.5, 2.5],
|
||||
genital: [1, 1, 1, 1, 1],
|
||||
r_uparm: [4, 4, 4, 4, 4],
|
||||
l_uparm: [4, 4, 4, 4, 4],
|
||||
r_forearm: [3, 3, 3, 3, 3],
|
||||
l_forearm: [3, 3, 3, 3, 3],
|
||||
r_hand: [2.5, 2.5, 2.5, 2.5, 2.5],
|
||||
l_hand: [2.5, 2.5, 2.5, 2.5, 2.5],
|
||||
r_thigh: [5.5, 8, 8.5, 9, 9.5],
|
||||
l_thigh: [5.5, 8, 8.5, 9, 9.5],
|
||||
r_leg: [5, 5.5, 6, 6.5, 7],
|
||||
l_leg: [5, 5.5, 6, 6.5, 7],
|
||||
r_foot: [3.5, 3.5, 3.5, 3.5, 3.5],
|
||||
l_foot: [3.5, 3.5, 3.5, 3.5, 3.5]
|
||||
};
|
||||
|
||||
// involvements: { regionKey: percentOfRegionInvolved (0-100), ... }
|
||||
function lundBrowderTBSA(ageBracket, involvements) {
|
||||
var idx = LB_BRACKETS.indexOf(ageBracket);
|
||||
if (idx < 0) return null;
|
||||
var total = 0;
|
||||
Object.keys(LB).forEach(function(key) {
|
||||
var pct = (involvements && involvements[key]) || 0;
|
||||
if (pct < 0) pct = 0; if (pct > 100) pct = 100;
|
||||
total += LB[key][idx] * (pct / 100);
|
||||
});
|
||||
return roundTo(total, 1);
|
||||
}
|
||||
|
||||
var API = {
|
||||
roundTo: roundTo,
|
||||
estimateWeightFromAgeMonths: estimateWeightFromAgeMonths,
|
||||
parkland: parkland,
|
||||
maintenance421: maintenance421,
|
||||
pramScore: pramScore,
|
||||
westleyScore: westleyScore,
|
||||
epiAnaphylaxisIM: epiAnaphylaxisIM,
|
||||
epiArrestIV: epiArrestIV,
|
||||
nrpEpiIV: nrpEpiIV,
|
||||
rsiKetamine: rsiKetamine,
|
||||
rsiRocuronium: rsiRocuronium,
|
||||
rsiSuccinylcholine: rsiSuccinylcholine,
|
||||
minSBP: minSBP,
|
||||
ettSize: ettSize,
|
||||
lundBrowderTBSA: lundBrowderTBSA,
|
||||
LB_BRACKETS: LB_BRACKETS,
|
||||
LB: LB
|
||||
};
|
||||
|
||||
if (typeof window !== 'undefined') window._PED_MATH = API;
|
||||
if (typeof module !== 'undefined' && module.exports) module.exports = API;
|
||||
})();
|
||||
File diff suppressed because it is too large
Load diff
167
test/calc-math.test.js
Normal file
167
test/calc-math.test.js
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
// ============================================================
|
||||
// Unit tests for public/js/calc-math.js (pediatric dose math).
|
||||
// Run with: npm test (alias for: node --test test/)
|
||||
// ============================================================
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const M = require('../public/js/calc-math.js');
|
||||
|
||||
// ── estimateWeightFromAgeMonths ────────────────────────────────
|
||||
test('APLS 0-12 mo: 6 mo -> 7 kg', () => {
|
||||
const r = M.estimateWeightFromAgeMonths(6);
|
||||
assert.equal(r.weight, 7); // 0.5*6 + 4 = 7
|
||||
assert.match(r.formulaLabel, /0-12 mo/);
|
||||
});
|
||||
|
||||
test('APLS 1-5 yr: 3 yr -> 14 kg', () => {
|
||||
const r = M.estimateWeightFromAgeMonths(36);
|
||||
assert.equal(r.weight, 14); // 2*3 + 8 = 14
|
||||
assert.match(r.formulaLabel, /1-5 yr/);
|
||||
});
|
||||
|
||||
test('APLS 6-12 yr: 8 yr -> 31 kg', () => {
|
||||
const r = M.estimateWeightFromAgeMonths(96);
|
||||
assert.equal(r.weight, 31); // 3*8 + 7 = 31
|
||||
assert.match(r.formulaLabel, /6-12 yr/);
|
||||
});
|
||||
|
||||
test('Best Guess 14 yr -> 56 kg (switches away from APLS at >=13 yr)', () => {
|
||||
const r = M.estimateWeightFromAgeMonths(168);
|
||||
assert.equal(r.weight, 56); // 4 * 14 = 56
|
||||
assert.match(r.formulaLabel, /Best Guess/);
|
||||
});
|
||||
|
||||
test('APLS and Best Guess both reported', () => {
|
||||
const r = M.estimateWeightFromAgeMonths(48);
|
||||
assert.equal(r.all.apls, 16); // 2*4 + 8
|
||||
assert.equal(r.all.bestGuess, 18); // 2*(4+5)
|
||||
});
|
||||
|
||||
test('Null/negative age returns null', () => {
|
||||
assert.equal(M.estimateWeightFromAgeMonths(null), null);
|
||||
assert.equal(M.estimateWeightFromAgeMonths(-5), null);
|
||||
});
|
||||
|
||||
// ── Parkland ────────────────────────────────────────────────────
|
||||
test('Parkland 20kg, 25% TBSA -> 2000 mL total, 1000 first 8h', () => {
|
||||
const p = M.parkland(20, 25);
|
||||
assert.equal(p.totalMl, 2000); // 4 * 20 * 25
|
||||
assert.equal(p.first8Ml, 1000);
|
||||
assert.equal(p.next16Ml, 1000);
|
||||
assert.equal(p.rateFirst8, 125); // 1000 / 8
|
||||
assert.equal(p.rateNext16, 63); // 1000 / 16 = 62.5 -> 63
|
||||
});
|
||||
|
||||
test('Parkland invalid inputs return null', () => {
|
||||
assert.equal(M.parkland(0, 25), null);
|
||||
assert.equal(M.parkland(20, 0), null);
|
||||
assert.equal(M.parkland(null, 25), null);
|
||||
});
|
||||
|
||||
// ── Maintenance 4-2-1 ──────────────────────────────────────────
|
||||
test('Maintenance: 8 kg -> 32 mL/hr', () => { assert.equal(M.maintenance421(8), 32); });
|
||||
test('Maintenance: 15 kg -> 50 mL/hr', () => { assert.equal(M.maintenance421(15), 50); });
|
||||
test('Maintenance: 25 kg -> 65 mL/hr', () => { assert.equal(M.maintenance421(25), 65); });
|
||||
test('Maintenance: 70 kg -> 110 mL/hr', () => { assert.equal(M.maintenance421(70), 110); });
|
||||
|
||||
// ── PRAM ────────────────────────────────────────────────────────
|
||||
test('PRAM all zero -> Mild (0)', () => {
|
||||
const r = M.pramScore(0, 0, 0, 0, 0);
|
||||
assert.equal(r.total, 0); assert.equal(r.severity, 'Mild');
|
||||
});
|
||||
test('PRAM total 5 -> Moderate', () => {
|
||||
const r = M.pramScore(1, 2, 0, 1, 1);
|
||||
assert.equal(r.total, 5); assert.equal(r.severity, 'Moderate');
|
||||
});
|
||||
test('PRAM total 10 -> Severe', () => {
|
||||
const r = M.pramScore(2, 2, 2, 2, 2);
|
||||
assert.equal(r.total, 10); assert.equal(r.severity, 'Severe');
|
||||
});
|
||||
|
||||
// ── Westley ─────────────────────────────────────────────────────
|
||||
test('Westley 2 -> Mild', () => { assert.equal(M.westleyScore(0, 0, 1, 0, 1).severity, 'Mild'); });
|
||||
test('Westley 4 -> Moderate', () => { assert.equal(M.westleyScore(0, 0, 2, 1, 1).severity, 'Moderate'); });
|
||||
test('Westley 8 -> Severe', () => { assert.equal(M.westleyScore(0, 4, 2, 1, 1).severity, 'Severe'); });
|
||||
test('Westley 12 -> Impending respiratory failure', () => { assert.equal(M.westleyScore(5, 4, 0, 1, 2).severity, 'Impending respiratory failure'); });
|
||||
|
||||
// ── Epinephrine (anaphylaxis vs arrest) ─────────────────────────
|
||||
test('Epi anaphylaxis IM: 20 kg -> 0.2 mg (1:1000)', () => {
|
||||
const r = M.epiAnaphylaxisIM(20);
|
||||
assert.equal(r.doseMg, 0.2);
|
||||
assert.equal(r.volumeMl_1in1000, 0.2); // 1 mg/mL concentration
|
||||
});
|
||||
|
||||
test('Epi anaphylaxis IM: max 0.5 mg for >=50 kg', () => {
|
||||
assert.equal(M.epiAnaphylaxisIM(80).doseMg, 0.5);
|
||||
});
|
||||
|
||||
test('Epi arrest IV: 10 kg -> 0.1 mg, 1 mL of 1:10,000', () => {
|
||||
const r = M.epiArrestIV(10);
|
||||
assert.equal(r.doseMg, 0.1);
|
||||
assert.equal(r.volumeMl_1in10000, 1); // 0.1 mg/mL concentration
|
||||
});
|
||||
|
||||
test('Epi arrest IV: max 1 mg for >=100 kg', () => {
|
||||
assert.equal(M.epiArrestIV(120).doseMg, 1);
|
||||
});
|
||||
|
||||
// ── NRP epi ──────────────────────────────────────────────────────
|
||||
test('NRP epi: 3 kg newborn -> 0.03-0.09 mg IV (0.3-0.9 mL of 1:10,000)', () => {
|
||||
const r = M.nrpEpiIV(3);
|
||||
assert.equal(r.lowMg, 0.03);
|
||||
assert.equal(r.highMg, 0.09);
|
||||
assert.equal(r.lowMl, 0.3);
|
||||
assert.equal(r.highMl, 0.9);
|
||||
});
|
||||
|
||||
// ── RSI drugs ────────────────────────────────────────────────────
|
||||
test('Ketamine RSI 20 kg -> 30-40 mg', () => {
|
||||
const r = M.rsiKetamine(20);
|
||||
assert.equal(r.lowMg, 30);
|
||||
assert.equal(r.highMg, 40);
|
||||
});
|
||||
|
||||
test('Rocuronium 25 kg -> 30 mg (1.2 mg/kg)', () => {
|
||||
assert.equal(M.rsiRocuronium(25).mg, 30);
|
||||
});
|
||||
|
||||
test('Succinylcholine dose/kg: <10 kg = 2 mg/kg; >=10 kg = 1.5 mg/kg', () => {
|
||||
assert.equal(M.rsiSuccinylcholine(8).perKg, 2);
|
||||
assert.equal(M.rsiSuccinylcholine(20).perKg, 1.5);
|
||||
assert.equal(M.rsiSuccinylcholine(20).mg, 30); // 20*1.5
|
||||
});
|
||||
|
||||
// ── Min systolic BP ─────────────────────────────────────────────
|
||||
test('Min SBP: 5 yr -> 80', () => { assert.equal(M.minSBP(5), 80); });
|
||||
test('Min SBP: infant (<1 yr) -> 70', () => { assert.equal(M.minSBP(0.5), 70); });
|
||||
test('Min SBP: 12 yr -> 90', () => { assert.equal(M.minSBP(12), 90); });
|
||||
|
||||
// ── ETT sizing ──────────────────────────────────────────────────
|
||||
test('ETT 4 yr: uncuffed 5.0, cuffed 4.5, depth 15', () => {
|
||||
const r = M.ettSize(4);
|
||||
assert.equal(r.uncuffedMm, 5);
|
||||
assert.equal(r.cuffedMm, 4.5);
|
||||
assert.equal(r.depthCm, 15);
|
||||
});
|
||||
|
||||
// ── Lund-Browder TBSA ───────────────────────────────────────────
|
||||
test('Lund-Browder: full head infant = 18%', () => {
|
||||
assert.equal(M.lundBrowderTBSA('infant', { head: 100 }), 18);
|
||||
});
|
||||
test('Lund-Browder: full head adult = 7%', () => {
|
||||
assert.equal(M.lundBrowderTBSA('adult', { head: 100 }), 7);
|
||||
});
|
||||
test('Lund-Browder: full body adds to ~100% (infant) — tolerance for published rounding', () => {
|
||||
const all = {};
|
||||
Object.keys(M.LB).forEach((k) => { all[k] = 100; });
|
||||
const t = M.lundBrowderTBSA('infant', all);
|
||||
// Published charts typically sum to 99-100% depending on head % (18 vs 19 at 0 yr vs 1 yr).
|
||||
assert.ok(t >= 98 && t <= 100.5, 'expected 98-100.5, got ' + t);
|
||||
});
|
||||
test('Lund-Browder: 50% of both thighs young child = 8%', () => {
|
||||
// young (1-5) thigh = 8% each, so 50% of each = 4% + 4% = 8%
|
||||
assert.equal(M.lundBrowderTBSA('young', { r_thigh: 50, l_thigh: 50 }), 8);
|
||||
});
|
||||
test('Lund-Browder: invalid bracket returns null', () => {
|
||||
assert.equal(M.lundBrowderTBSA('bogus', { head: 50 }), null);
|
||||
});
|
||||
Loading…
Reference in a new issue