pediatric-ai-scribe-v3/public/js/bedside/burns.js
Daniel ddf9ca805b feat: C — extract Bedside reference into ES modules
Split the Bedside clinical reference section out of calculators.js
(~1220 lines) into 20 focused ES modules under public/js/bedside/.
Each module owns one clinical topic (cardiac, seizure, sepsis, burns,
etc.) and exports an init() wired up by bedside/index.js. Shared
helpers live in shared.js and also set window._EM for back-compat
with the 4 remaining call sites in calculators.js.

Load order: classic defer calculators.js first, then module script
bedside/index.js. Handlers bind at DOM-ready; runtime _EM lookups
resolve after both are evaluated.

Makes bedside content editable per-section, shrinks calculators.js
from 4111 to 2891 lines, and keeps the existing Playwright smoke
suite (e2e/tests/bedside-smoke.spec.js) as the behavioral contract.
2026-04-20 23:19:50 +02:00

155 lines
9.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ============================================================
// bedside/burns.js
// BURNS — Lund-Browder body parts + Parkland formula.
// ============================================================
import { S } from './shared.js';
// Lund-Browder age-adjusted region percentages. Regions marked "*" change with age.
// Column order: infant, young (1-5), child (5-10), adol (10-15), adult (>15).
var LB = {
head: { label: 'Head', vals: [18, 13, 11, 9, 7], ageSensitive: true },
neck: { label: 'Neck', vals: [2, 2, 2, 2, 2] },
ant_trunk: { label: 'Anterior trunk', vals: [13, 13, 13, 13, 13] },
post_trunk:{ label: 'Posterior trunk', vals: [13, 13, 13, 13, 13] },
r_buttock: { label: 'Right buttock', vals: [2.5, 2.5, 2.5, 2.5, 2.5] },
l_buttock: { label: 'Left buttock', vals: [2.5, 2.5, 2.5, 2.5, 2.5] },
genital: { label: 'Genitalia', vals: [1, 1, 1, 1, 1] },
r_uparm: { label: 'R upper arm', vals: [4, 4, 4, 4, 4] },
l_uparm: { label: 'L upper arm', vals: [4, 4, 4, 4, 4] },
r_forearm: { label: 'R forearm', vals: [3, 3, 3, 3, 3] },
l_forearm: { label: 'L forearm', vals: [3, 3, 3, 3, 3] },
r_hand: { label: 'R hand', vals: [2.5, 2.5, 2.5, 2.5, 2.5] },
l_hand: { label: 'L hand', vals: [2.5, 2.5, 2.5, 2.5, 2.5] },
r_thigh: { label: 'R thigh', vals: [5.5, 8, 8.5, 9, 9.5], ageSensitive: true },
l_thigh: { label: 'L thigh', vals: [5.5, 8, 8.5, 9, 9.5], ageSensitive: true },
r_leg: { label: 'R lower leg', vals: [5, 5.5, 6, 6.5, 7], ageSensitive: true },
l_leg: { label: 'L lower leg', vals: [5, 5.5, 6, 6.5, 7], ageSensitive: true },
r_foot: { label: 'R foot', vals: [3.5, 3.5, 3.5, 3.5, 3.5] },
l_foot: { label: 'L foot', vals: [3.5, 3.5, 3.5, 3.5, 3.5] }
};
var AGE_KEYS = ['infant','young','child','adol','adult'];
function ageIdx() { return Math.max(0, AGE_KEYS.indexOf(document.getElementById('burn-age').value)); }
function renderBodyParts() {
var wrap = document.getElementById('burn-bodyparts-wrapper');
if (!wrap) return;
var idx = ageIdx();
var html = '<div style="background:var(--g50);border-radius:8px;padding:10px 12px;">';
html += '<div style="font-size:12px;color:var(--g600);margin-bottom:8px;line-height:1.4;">For each involved body region, enter <strong>% of that region burned (2° or deeper)</strong>. Leave 0 if uninvolved. TBSA = sum of (region size × % involvement).</div>';
html += '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:6px;">';
Object.keys(LB).forEach(function(key) {
var r = LB[key];
var max = r.vals[idx];
var isAgeDep = r.ageSensitive;
html += '<div style="display:flex;align-items:center;gap:8px;padding:4px 8px;background:white;border-radius:6px;border:1px solid var(--g200);">' +
'<label style="flex:1;font-size:12px;color:var(--g700);">' + r.label + ' <span style="color:var(--g400);">(' + max + '%' + (isAgeDep ? '<span title="age-adjusted">*</span>' : '') + ')</span></label>' +
'<input type="number" min="0" max="100" step="5" value="0" data-burn-region="' + key + '" data-burn-max="' + max + '" style="width:68px;padding:3px 6px;border:1px solid var(--g300);border-radius:4px;font-size:12px;text-align:right;"> %' +
'</div>';
});
html += '</div></div>';
wrap.innerHTML = html;
updateLive();
}
function resetParts() {
var inputs = document.querySelectorAll('[data-burn-region]');
inputs.forEach(function(i) { i.value = 0; });
document.getElementById('burn-tbsa').value = '';
updateLive();
}
function computeTbsa() {
var idx = ageIdx();
var total = 0;
Object.keys(LB).forEach(function(key) {
var el = document.querySelector('[data-burn-region="' + key + '"]');
if (!el) return;
var pct = parseFloat(el.value) || 0;
if (pct < 0) pct = 0; if (pct > 100) pct = 100;
total += LB[key].vals[idx] * (pct / 100);
});
return Math.round(total * 10) / 10;
}
function updateLive() {
var t = computeTbsa();
var live = document.getElementById('burn-tbsa-live');
if (live) live.textContent = t > 0 ? 'Computed TBSA: ' + t + '%' : '';
}
function calcBurn() {
var wt = parseFloat(document.getElementById('burn-weight').value);
var override = document.getElementById('burn-tbsa').value;
var tbsa = override !== '' ? parseFloat(override) : computeTbsa();
var el = document.getElementById('burn-result');
if (!wt) { el.innerHTML = '<p style="color:var(--red);font-size:13px;">Enter weight (kg).</p>'; return; }
if (!tbsa || tbsa <= 0) { el.innerHTML = '<p style="color:var(--red);font-size:13px;">Enter % involvement per region, or override TBSA.</p>'; return; }
var total = Math.round(4 * wt * tbsa);
var first8 = Math.round(total / 2);
var rateFirst = Math.round(first8 / 8);
var next16 = total - first8;
var rateNext = Math.round(next16 / 16);
var maint = wt <= 10 ? wt * 4 : wt <= 20 ? 40 + (wt - 10) * 2 : 60 + (wt - 20);
maint = Math.round(maint);
var html = '<div style="padding:10px 14px;border-radius:8px;background:#fee2e2;border:1.5px solid #ef4444;margin-bottom:10px;"><strong style="color:#dc2626;">Burn fluid resuscitation — ' + wt + ' kg, ' + tbsa + '% TBSA (2° or deeper)</strong></div>';
html += '<div style="padding:12px;background:#fef2f2;border-radius:6px;margin-bottom:10px;"><strong>Parkland formula:</strong> 4 mL × kg × %TBSA = <strong>' + total + ' mL LR over 24 hours</strong>' +
'<br>&nbsp;&nbsp;<strong>First 8 h</strong> (from time of burn): ' + first8 + ' mL (~<strong>' + rateFirst + ' mL/hr</strong>)' +
'<br>&nbsp;&nbsp;<strong>Next 16 h</strong>: ' + next16 + ' mL (~<strong>' + rateNext + ' mL/hr</strong>)</div>';
html += '<div style="padding:12px;background:var(--g50);border-radius:6px;margin-bottom:10px;"><strong>Plus maintenance fluids (4-2-1):</strong> ' + maint + ' mL/hr (D5 1/2NS ± 20 mEq KCl/L once UOP established). Consider dextrose in children &lt;30 kg.</div>';
html += '<div style="padding:12px;background:var(--g50);border-radius:6px;margin-bottom:10px;"><strong>Titrate to UOP:</strong> target 1-2 mL/kg/hr (infants / children), 0.5-1 mL/kg/hr (adolescents). <strong>Clinical response trumps formula.</strong></div>';
// Breakdown of what regions contributed
var idx = ageIdx();
var breakdown = '';
Object.keys(LB).forEach(function(key) {
var el2 = document.querySelector('[data-burn-region="' + key + '"]');
if (!el2) return;
var pct = parseFloat(el2.value) || 0;
if (pct > 0) {
var contrib = Math.round(LB[key].vals[idx] * pct / 100 * 10) / 10;
breakdown += '<div>' + LB[key].label + ': ' + pct + '% of ' + LB[key].vals[idx] + '% = ' + contrib + '%</div>';
}
});
if (breakdown) {
html += '<details style="margin-bottom:10px;"><summary style="cursor:pointer;font-size:13px;font-weight:600;color:var(--g700);">Region breakdown</summary><div style="padding:10px;background:var(--g50);border-radius:6px;font-size:12px;margin-top:6px;">' + breakdown + '</div></details>';
}
html += '<h5 style="font-size:13px;font-weight:700;color:var(--g700);margin:14px 0 6px;">Other pearls</h5>';
html += '<div style="padding:10px;background:var(--g50);border-radius:6px;font-size:12px;">';
html += '<strong>Rule of palm:</strong> Patient\'s palm + fingers ≈ 1% TBSA — good for scattered burns.<br>';
html += '<strong>First-degree burns DO NOT count</strong> toward TBSA or Parkland.<br>';
html += '<strong>Analgesia:</strong> Morphine 0.05-0.1 mg/kg IV q2h, or fentanyl 1-2 mcg/kg IV q30-60 min.<br>';
html += '<strong>Tetanus</strong> prophylaxis if indicated. Tdap/Td ± tetanus immunoglobulin.';
html += '</div>';
html += '<h5 style="font-size:13px;font-weight:700;color:var(--g700);margin:14px 0 6px;">Burn center referral (ABA)</h5>';
html += '<div style="padding:10px;background:#fef3c7;border-radius:6px;font-size:12px;color:#92400e;">Partial-thickness &gt;10% TBSA; any full-thickness; face/hands/feet/genital/perineum/major joints; electrical/chemical/inhalation; associated trauma; significant comorbidities; pediatric burns in non-pediatric center.</div>';
html += S.ref('ABA Advanced Burn Life Support (ABLS) 2018. Parkland formula: Baxter 1968. Lund-Browder 1944 chart (age-adjusted regions).');
el.innerHTML = html;
}
export function init() {
document.addEventListener('click', function(e) {
if (e.target.id === 'btn-burn-calc' || e.target.closest('#btn-burn-calc')) calcBurn();
if (e.target.id === 'btn-burn-reset' || e.target.closest('#btn-burn-reset')) resetParts();
// Render body-parts table the first (or any) time the Burns sub-pill is activated
var bp = e.target.closest && e.target.closest('[data-em="burns"]');
if (bp) setTimeout(renderBodyParts, 0);
// Also render if the top-level Bedside tab is clicked (ensures table ready before user navigates)
var nav = e.target.closest && e.target.closest('[data-calc="bedside"]');
if (nav) setTimeout(renderBodyParts, 50);
});
document.addEventListener('change', function(e) {
if (e.target && e.target.id === 'burn-age') renderBodyParts();
});
document.addEventListener('input', function(e) {
if (e.target && e.target.dataset && e.target.dataset.burnRegion) updateLive();
});
// Also attempt render on these events in case component mounts later
document.addEventListener('DOMContentLoaded', renderBodyParts);
document.addEventListener('tabChanged', function() { setTimeout(renderBodyParts, 100); });
if (document.readyState !== 'loading') setTimeout(renderBodyParts, 0);
}