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.
155 lines
9.5 KiB
JavaScript
155 lines
9.5 KiB
JavaScript
// ============================================================
|
||
// 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> <strong>First 8 h</strong> (from time of burn): ' + first8 + ' mL (~<strong>' + rateFirst + ' mL/hr</strong>)' +
|
||
'<br> <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 <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 >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);
|
||
}
|