diff --git a/public/e2e-harness.html b/public/e2e-harness.html index d109f19..63e9d1c 100644 --- a/public/e2e-harness.html +++ b/public/e2e-harness.html @@ -18,6 +18,7 @@ + diff --git a/public/index.html b/public/index.html index 6d5807f..3eae536 100644 --- a/public/index.html +++ b/public/index.html @@ -447,6 +447,7 @@ + diff --git a/public/js/bedside/age-weight.js b/public/js/bedside/age-weight.js new file mode 100644 index 0000000..9a11882 --- /dev/null +++ b/public/js/bedside/age-weight.js @@ -0,0 +1,80 @@ +// ============================================================ +// bedside/age-weight.js +// BEDSIDE — AGE → WEIGHT (single source for all sub-sections). +// Top estimator wiring: #bedside-age, #bedside-formula, +// #bedside-weight, #btn-bedside-clear. +// ============================================================ + +export function init() { + document.addEventListener('input', function(e) { + if (e.target && e.target.id === 'bedside-age') recomputeFromAge(); + if (e.target && e.target.id === 'bedside-weight') { + e.target.dataset.userTyped = '1'; + } + }); + document.addEventListener('change', function(e) { + if (e.target && e.target.id === 'bedside-formula') recomputeFromAge(true); + }); + document.addEventListener('click', function(e) { + if (e.target.closest && e.target.closest('#btn-bedside-clear')) { + document.getElementById('bedside-age').value = ''; + var wtField = document.getElementById('bedside-weight'); + wtField.value = ''; + delete wtField.dataset.userTyped; + var note = document.getElementById('bedside-estimate-note'); + if (note) note.innerHTML = ''; + } + }); + + // Recompute weight from age using the currently selected formula. + // If forceFromFormula is true (formula dropdown changed), overwrite even user-typed weight. + function recomputeFromAge(forceFromFormula) { + var raw = document.getElementById('bedside-age').value; + var note = document.getElementById('bedside-estimate-note'); + if (!raw || !raw.trim()) { if (note) note.innerHTML = ''; return; } + var months = window._parseAgeMonths ? window._parseAgeMonths(raw) : null; + if (months == null || isNaN(months) || months < 0) { + if (note) note.innerHTML = 'Could not parse age. Try "3y", "18 months", "15 days".'; + return; + } + var formula = document.getElementById('bedside-formula').value; + var est = window._PED_MATH.estimateWeightFromAgeMonths(months); + var pickedWeight = formula === 'bestguess' ? est.all.bestGuess : est.all.apls; + var pickedLabel = formula === 'bestguess' ? bestGuessBand(months) : aplsBand(months); + + var wtField = document.getElementById('bedside-weight'); + if (forceFromFormula || !wtField.dataset.userTyped) { + wtField.value = pickedWeight; + delete wtField.dataset.userTyped; // treat as computed again + } + var ageTxt = months < 12 + ? (Math.round(months * 10) / 10) + ' months' + : (Math.round(months / 12 * 10) / 10) + ' years'; + if (note) note.innerHTML = '' + pickedWeight + ' kg ' + + '(' + ageTxt + ', ' + pickedLabel + ') · ' + + 'APLS: ' + est.all.apls + ' kg · Best Guess: ' + est.all.bestGuess + ' kg. You can override the weight field.'; + } + + function aplsBand(m) { + if (m < 12) return 'APLS 0-12 mo'; + var y = m / 12; + if (y <= 5) return 'APLS 1-5 yr'; + return 'APLS 6-12 yr'; + } + function bestGuessBand(m) { + if (m < 12) return 'Best Guess 1-11 mo'; + var y = m / 12; + if (y <= 5) return 'Best Guess 1-4 yr'; + return 'Best Guess 5-14 yr'; + } + + // Top bedside weight is estimator-only. Sections have their own weight fields and + // read them directly (no auto-propagation). Expose the reader on window for + // safety (some older code paths may look it up). + if (typeof window !== 'undefined') { + window._getBedsideWeight = function() { + var el = document.getElementById('bedside-weight'); + return el ? parseFloat(el.value) || null : null; + }; + } +} diff --git a/public/js/bedside/agitation.js b/public/js/bedside/agitation.js new file mode 100644 index 0000000..c54dc69 --- /dev/null +++ b/public/js/bedside/agitation.js @@ -0,0 +1,83 @@ +// ============================================================ +// bedside/agitation.js +// AGITATION — step 1/2/3 pathway, drugs.json-backed. +// ============================================================ + +import { S } from './shared.js'; + +// Fallback inline data — mirrors drugs.json agitation section, split into +// step-2 (PO/IN) and step-3 (IV/IM) subgroups by the route field. +// Display-name column intentionally drops the route suffix from the JSON +// name so the table still shows "Lorazepam" / "Midazolam" / "Ketamine". +var AGIT_FALLBACK = [ + // Step 2 — PO / IN + { name: 'Lorazepam PO', display: 'Lorazepam', step: 2, dose_mg_per_kg: 0.05, max_mg: 2, unit: 'mg', route: 'PO', notes: '0.05 mg/kg. May repeat in 30 min.' }, + { name: 'Midazolam PO', display: 'Midazolam', step: 2, dose_mg_per_kg: 0.5, max_mg: 10, unit: 'mg', route: 'PO', notes: '0.5 mg/kg (max 10 mg)' }, + { name: 'Midazolam IN', display: 'Midazolam', step: 2, dose_mg_per_kg: 0.3, max_mg: 10, unit: 'mg', route: 'IN (5 mg/mL)', notes: '0.3 mg/kg split between nares (max 10 mg)' }, + { name: 'Olanzapine (ODT)', display: 'Olanzapine (ODT)', step: 2, dose_mg_per_kg: null, max_mg: null, unit: 'mg', route: 'PO / ODT', notes: 'Age ≥6 yr. Avoid IM + benzo combo (risk of resp depression).', dose_display_if_under_30kg: '2.5-5 mg', dose_display_if_30kg_or_more: '5-10 mg' }, + { name: 'Diphenhydramine PO', display: 'Diphenhydramine', step: 2, dose_mg_per_kg: 1, max_mg: 50, unit: 'mg', route: 'PO', notes: '1 mg/kg. Adjunct only; sedating.' }, + // Step 3 — IV / IM + { name: 'Midazolam IV/IM', display: 'Midazolam', step: 3, dose_mg_per_kg: 0.1, max_mg: 5, unit: 'mg', route: 'IV / IM', notes: '0.05-0.1 mg/kg IV, 0.1-0.15 mg/kg IM (max 10 mg)' }, + { name: 'Lorazepam IV/IM', display: 'Lorazepam', step: 3, dose_mg_per_kg: 0.1, max_mg: 4, unit: 'mg', route: 'IV / IM', notes: '0.05-0.1 mg/kg (max 4 mg). May cause resp depression.' }, + { name: 'Haloperidol', display: 'Haloperidol', step: 3, dose_mg_per_kg: 0.05, max_mg: null, unit: 'mg', route: 'IM / IV', notes: 'Avoid <3 yr. Risk: QT, EPS, NMS. ECG if repeated.', dose_display_if_under_40kg: '0.025-0.075 mg/kg', dose_display_if_40kg_or_more: '2.5-5 mg' }, + { name: 'Olanzapine IM', display: 'Olanzapine', step: 3, dose_mg_per_kg: null, max_mg: null, unit: 'mg', route: 'IM', notes: 'Avoid benzo co-administration (resp depression, hypotension)', dose_display_if_under_40kg: '2.5-5 mg', dose_display_if_40kg_or_more: '5-10 mg' }, + { name: 'Ketamine IM', display: 'Ketamine', step: 3, dose_mg_per_kg: 4, max_mg: 500, unit: 'mg', route: 'IM', notes: '4-5 mg/kg IM (rescue for severe excited delirium). Monitor airway.' }, + { name: 'Droperidol', display: 'Droperidol', step: 3, dose_mg_per_kg: 0.05, max_mg: null, unit: 'mg', route: 'IM / IV', notes: 'Effective but QT concern — get ECG.', dose_display_prefix: '0.03-0.07 mg/kg' } +]; + +function agitDrugs() { + var s = window._DRUGS && window._DRUGS.sections && window._DRUGS.sections.agitation; + if (s && s.drugs && s.drugs.length) { + // Merge per-drug hints from fallback (display name, step) when the + // JSON doesn't carry them — keeps rendering identical. + return s.drugs.map(function(dg) { + var fb = AGIT_FALLBACK.filter(function(x) { return x.name === dg.name; })[0] || {}; + return Object.assign({}, fb, dg); + }); + } + return AGIT_FALLBACK; +} + +// Render one drug row, handling the agitation-specific display quirks: +// weight-conditional dose strings (Olanzapine, Haloperidol) and hand- +// computed "prefix = value mg" strings (Droperidol, Haloperidol <40 kg). +function agitRowFor(wt, dg) { + var dose; + if (dg.dose_display_if_under_30kg && dg.dose_display_if_30kg_or_more) { + dose = wt < 30 ? dg.dose_display_if_under_30kg : dg.dose_display_if_30kg_or_more; + } else if (dg.dose_display_if_under_40kg && dg.dose_display_if_40kg_or_more) { + dose = wt < 40 ? dg.dose_display_if_under_40kg + ' = ' + S.d(wt, dg.dose_mg_per_kg) + ' mg' : dg.dose_display_if_40kg_or_more; + } else if (dg.dose_display_prefix) { + dose = dg.dose_display_prefix + ' = ' + S.d(wt, dg.dose_mg_per_kg) + ' mg'; + } else if (dg.dose_mg_per_kg != null) { + dose = S.dStr(wt, dg.dose_mg_per_kg, dg.max_mg, ' ' + dg.unit); + } else { + dose = dg.dose_display || ''; + } + return S.drugRow(dg.display || dg.name, dose, dg.route, dg.notes); +} + +function calcAgit() { + var wt = parseFloat(document.getElementById('agit-weight').value); + var el = document.getElementById('agit-result'); + if (!wt) { el.innerHTML = '

Enter weight (kg).

'; return; } + var drugs = agitDrugs(); + var step2 = drugs.filter(function(dg) { return dg.step === 2; }); + var step3 = drugs.filter(function(dg) { return dg.step === 3; }); + + var html = '
Agitation management — ' + wt + ' kg
'; + html += S.stepBox('#10b981', 'Step 1 — Non-pharmacologic', 'Dim lights, quiet room, familiar caregiver present, reduce stimulation. Rule out hypoglycemia, hypoxia, pain, ↑ICP, toxic ingestion, infection.'); + html += S.stepBox('#3b82f6', 'Step 2 — Oral / intranasal (cooperative)', 'First-line for moderate agitation if the patient will accept PO/IN.'); + html += S.drugTable(step2.map(function(dg) { return agitRowFor(wt, dg); }).join('')); + html += S.stepBox('#f59e0b', 'Step 3 — IM / IV (severe, uncooperative, or safety risk)', 'Require continuous monitoring. Consider restraints only as last resort.'); + html += S.drugTable(step3.map(function(dg) { return agitRowFor(wt, dg); }).join('')); + html += '
Monitoring: Continuous SpO2 + HR after parenteral sedation. Have airway equipment, flumazenil, naloxone, and IV access ready.
'; + html += S.ref('AAP Clinical Report on Pediatric Agitation. ACEP Guidelines for Acute Agitation.'); + el.innerHTML = html; +} + +export function init() { + document.addEventListener('click', function(e) { + if (e.target.id === 'btn-agit-calc' || e.target.closest('#btn-agit-calc')) calcAgit(); + }); +} diff --git a/public/js/bedside/airway.js b/public/js/bedside/airway.js new file mode 100644 index 0000000..d208452 --- /dev/null +++ b/public/js/bedside/airway.js @@ -0,0 +1,90 @@ +// ============================================================ +// bedside/airway.js +// AIRWAY / RSI. +// ============================================================ + +import { S } from './shared.js'; + +function calcAirway() { + var wt = parseFloat(document.getElementById('airway-weight').value); + var age = parseFloat(document.getElementById('airway-age').value); + var el = document.getElementById('airway-result'); + if (!wt) { el.innerHTML = '

Enter weight (kg).

'; return; } + + // Equipment sizing + var ettUncuff = age ? Math.round(((age/4) + 4) * 2) / 2 : null; + var ettCuff = age ? Math.round(((age/4) + 3.5) * 2) / 2 : null; + var ettDepth = ettUncuff ? Math.round(ettUncuff * 3 * 10) / 10 : null; + var bladeSize = age != null ? (age < 1 ? 'Miller 0-1' : age < 2 ? 'Miller 1 / Mac 1' : age < 8 ? 'Miller 2 / Mac 2' : 'Miller/Mac 3') : '—'; + var lmaSize = wt < 5 ? '1' : wt < 10 ? '1.5' : wt < 20 ? '2' : wt < 30 ? '2.5' : wt < 50 ? '3' : wt < 70 ? '4' : '5'; + + var html = '
RSI doses & equipment — ' + wt + ' kg' + (age ? ', ~' + age + ' yr' : '') + '
'; + + // Pre-medication + html += '
Pre-medication (optional)
'; + html += S.drugTable( + S.drugRow('Atropine', S.dStr(wt, 0.02, 1, ' mg') + ' (min 0.1 mg)', 'IV', 'Consider <1 yr or if using succinylcholine, or bradycardia risk') + + S.drugRow('Lidocaine 2%', S.dStr(wt, 1.5, 100, ' mg'), 'IV over 1 min', 'For ↑ICP, asthma; give 2-3 min pre-induction') + + S.drugRow('Fentanyl', S.dStr(wt, 2, 150, ' mcg') + ' (2-3 mcg/kg)', 'IV over 1 min', 'Blunts sympathetic response; avoid if hypotensive') + ); + + // Induction + html += '
Induction
'; + html += S.drugTable( + S.drugRow('Ketamine', S.dStr(wt, 1.5, 150, ' mg') + ' (1-2 mg/kg)', 'IV', 'Preserves BP. First-line for shock, asthma, sepsis') + + S.drugRow('Etomidate', S.dStr(wt, 0.3, 30, ' mg'), 'IV', 'Hemodynamically neutral. Avoid in septic shock (adrenal suppression)') + + S.drugRow('Propofol', S.dStr(wt, 2, 200, ' mg') + ' (1-2 mg/kg)', 'IV', 'Causes hypotension. Avoid in shock') + + S.drugRow('Midazolam', S.dStr(wt, 0.2, 10, ' mg'), 'IV', 'Less optimal induction — slower onset') + ); + + // Paralytics + html += '
Paralytics
'; + html += S.drugTable( + S.drugRow('Rocuronium', S.dStr(wt, 1.2, 100, ' mg') + ' (1-1.2 mg/kg)', 'IV', '30-60 sec onset; 30-45 min duration. First-line non-depolarizing.') + + S.drugRow('Succinylcholine', S.dStr(wt, (wt < 10 ? 2 : 1.5), 150, ' mg'), 'IV', '<10kg: 2 mg/kg; ≥10kg: 1.5 mg/kg. Avoid in burns/crush/hyperK/MH/NM dz. IM option: 4 mg/kg.') + + S.drugRow('Vecuronium', S.dStr(wt, 0.1, 10, ' mg'), 'IV', '2-3 min onset; longer duration than roc') + ); + + // Post-intubation sedation + html += '
Maintenance sedation / analgesia
'; + html += S.drugTable( + S.drugRow('Midazolam gtt', S.dStr(wt, 0.05, 2, ' mg') + '-' + S.dStr(wt, 0.2, 10, ' mg') + '/hr', 'IV infusion', '0.05-0.2 mg/kg/hr') + + S.drugRow('Fentanyl gtt', S.dStr(wt, 1, 100, ' mcg') + '-' + S.dStr(wt, 4, 200, ' mcg') + '/hr', 'IV infusion', '1-4 mcg/kg/hr') + + S.drugRow('Ketamine gtt', S.dStr(wt, 0.5, 30, ' mg') + '-' + S.dStr(wt, 2, 100, ' mg') + '/hr', 'IV infusion', '0.5-2 mg/kg/hr. Good for asthma') + + S.drugRow('Dexmedetomidine gtt', '0.2-1.4 mcg/kg/hr', 'IV infusion', 'No resp depression; causes bradycardia + hypotension') + + S.drugRow('Rocuronium gtt', '10-12 mcg/kg/min', 'IV infusion', 'If continued paralysis needed — only with adequate sedation') + ); + + // Equipment + html += '
Equipment sizing
'; + html += '
'; + if (age != null) { + html += '
ETT (uncuffed): ' + ettUncuff + ' mm
ETT (cuffed): ' + ettCuff + ' mm
Depth at lip: ~' + ettDepth + ' cm
'; + html += '
Laryngoscope blade:
' + bladeSize + '
'; + } else { + html += '
Enter age for ETT/blade sizing.
ETT (uncuffed) = (age/4) + 4
ETT (cuffed) = (age/4) + 3.5
Depth = ETT size × 3
'; + } + html += '
LMA size: ' + lmaSize + '
OPA = corner mouth → angle mandible
NPA = tip nose → tragus
'; + html += '
Suction catheter (Fr): ETT size × 2
NG tube: ETT × 2
Chest tube: 4 × ETT
'; + html += '
'; + + // Ventilator starting settings + html += '
Ventilator — starting settings
'; + html += '
'; + html += 'Mode: Volume or pressure control
'; + html += 'Tidal volume: 6-8 mL/kg (= ' + S.d(wt, 6) + '-' + S.d(wt, 8) + ' mL). Use lower (4-6 mL/kg) for ARDS.
'; + html += 'Rate: Infant 25-30, Child 15-25, Adolescent 12-16
'; + html += 'PEEP: 5 cmH2O (↑ for oxygenation problems)
'; + html += 'FiO2: Start 100%, wean to SpO2 92-97%
'; + html += 'I:E ratio: 1:2 (1:3-4 for obstructive disease)'; + html += '
'; + + html += S.ref('Harriet Lane Handbook 23rd ed / PALS 2020. Always confirm doses against institutional protocols.'); + el.innerHTML = html; +} + +export function init() { + document.addEventListener('click', function(e) { + if (e.target.id === 'btn-airway-calc' || e.target.closest('#btn-airway-calc')) calcAirway(); + }); +} diff --git a/public/js/bedside/anaphylaxis.js b/public/js/bedside/anaphylaxis.js new file mode 100644 index 0000000..450ebeb --- /dev/null +++ b/public/js/bedside/anaphylaxis.js @@ -0,0 +1,82 @@ +// ============================================================ +// bedside/anaphylaxis.js +// ANAPHYLAXIS MANAGEMENT. +// ============================================================ + +import { S } from './shared.js'; + +function d(wt, mgPerKg, max) { var v = Math.round(wt * mgPerKg * 100) / 100; return max ? Math.min(v, max) : v; } + +// Lookup a drug entry (by name) from window._DRUGS.sections.anaphylaxis, +// falling back to a hardcoded record so rendering stays identical if the +// JSON fetch hasn't resolved (or failed). Returns an object exposing the +// per-kg + max fields needed by S.dStr. +var ANAPH_FALLBACK = { + 'Epinephrine IM': { dose_mg_per_kg: 0.01, max_mg: 0.5, unit: 'mg' }, + 'Normal saline bolus': { dose_mg_per_kg: 20, max_mg: null, unit: 'mL' }, + 'Diphenhydramine': { dose_mg_per_kg: 1.25, max_mg: 50, unit: 'mg' }, + 'Ranitidine': { dose_mg_per_kg: 1, max_mg: 50, unit: 'mg' }, + 'Dexamethasone': { dose_mg_per_kg: 0.6, max_mg: 16, unit: 'mg' }, + 'Methylprednisolone': { dose_mg_per_kg: 2, max_mg: 125, unit: 'mg' }, + 'Glucagon': { dose_mg_per_kg: 0.02, max_mg: 1, unit: 'mg' } +}; + +function anaphDrug(name) { + var s = window._DRUGS && window._DRUGS.sections && window._DRUGS.sections.anaphylaxis; + if (s && s.drugs) { + var hit = s.drugs.filter(function(x) { return x.name === name; })[0]; + if (hit) return hit; + } + return ANAPH_FALLBACK[name] || {}; +} + +function calcAnaphylaxis() { + var wt = parseFloat(document.getElementById('anaph-weight').value); + var age = document.getElementById('anaph-age') ? document.getElementById('anaph-age').value : null; // kept for parity; not currently branched on + void age; + var resultDiv = document.getElementById('anaph-result'); + if (!wt) { resultDiv.innerHTML = '

Enter weight (kg).

'; return; } + + var epi = anaphDrug('Epinephrine IM'); + var fluids = anaphDrug('Normal saline bolus'); + var dph = anaphDrug('Diphenhydramine'); + var rnt = anaphDrug('Ranitidine'); + var dex = anaphDrug('Dexamethasone'); + var mpn = anaphDrug('Methylprednisolone'); + var glc = anaphDrug('Glucagon'); + + var epiDose = d(wt, epi.dose_mg_per_kg, epi.max_mg); + var epiVol = Math.round(epiDose * 10) / 10; // 1:1000 = 1 mg/mL + var autoInjector = wt < 10 ? 'Draw up manually' : wt <= 25 ? 'EpiPen Jr / Auvi-Q 0.15 mg' : 'EpiPen / Auvi-Q 0.3 mg'; + + var html = '
'; + html += '
STEP 1: Epinephrine IM — GIVE IMMEDIATELY
'; + html += '
Epinephrine 1:1000 (1 mg/mL): ' + epiDose + ' mg (' + epiVol + ' mL) IM to lateral thigh
'; + html += '
' + epi.dose_mg_per_kg + ' mg/kg (max ' + epi.max_mg + ' mg) | Auto-injector: ' + autoInjector + '
'; + html += '
May repeat every 5-15 minutes if symptoms persist. No contraindications in anaphylaxis.
'; + html += '
'; + + html += '
'; + + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + + html += ''; + html += ''; + + html += '
StepDrugDoseRouteNotes
STEP 2PositionSupine + legs elevatedIf dyspneic: sitting position. If vomiting: recovery position
STEP 3O2High flow 10-15 L/minFace mask100% O2. Prepare for airway management
STEP 4IV fluidsNS ' + S.dStr(wt, fluids.dose_mg_per_kg, fluids.max_mg, ' mL') + ' bolusIV/IORepeat up to 60 mL/kg for hypotension
STEP 5Diphenhydramine' + S.dStr(wt, dph.dose_mg_per_kg, dph.max_mg) + 'IV/IM/POH1 blocker. NOT first-line — adjunct only
Ranitidine' + S.dStr(wt, rnt.dose_mg_per_kg, rnt.max_mg) + 'IV over 5 minH2 blocker. Optional adjunct
STEP 6Dexamethasone' + S.dStr(wt, dex.dose_mg_per_kg, dex.max_mg) + 'IV/IM/POPrevents biphasic reaction (4-6 hrs later)
Methylprednisolone' + S.dStr(wt, mpn.dose_mg_per_kg, mpn.max_mg) + 'IVAlternative steroid
IF REFRACTORYEpinephrine gtt0.1-1 mcg/kg/minIV infusionFor persistent hypotension despite fluids + IM epi
Glucagon' + S.dStr(wt, glc.dose_mg_per_kg, glc.max_mg) + 'IV/IMFor patients on beta-blockers not responding to epi
'; + html += '
Observe minimum 4-6 hours after last dose of epinephrine (biphasic reactions occur in 5-20% of cases). Discharge with EpiPen prescription and anaphylaxis action plan. Refer to allergist.
'; + html += '

ASCIA Anaphylaxis Guidelines 2021. WAO Anaphylaxis Guidance 2020. AAP/ACAAI Practice Parameters.

'; + resultDiv.innerHTML = html; +} + +export function init() { + document.addEventListener('click', function(e) { + if (e.target.id === 'btn-anaph-calc' || e.target.closest('#btn-anaph-calc')) calcAnaphylaxis(); + }); +} diff --git a/public/js/bedside/antiemetics.js b/public/js/bedside/antiemetics.js new file mode 100644 index 0000000..df8efa6 --- /dev/null +++ b/public/js/bedside/antiemetics.js @@ -0,0 +1,59 @@ +// ============================================================ +// bedside/antiemetics.js +// ANTIEMETICS — drugs.json-backed. +// ============================================================ + +import { S } from './shared.js'; + +// Fallback inline data — used if /data/drugs.json hasn't loaded yet. +// Must match drugs.json shape for the antiemetics section. +var EMET_FALLBACK = [ + { name: 'Ondansetron', dose_mg_per_kg: 0.15, max_mg: 8, unit: 'mg', route: 'PO / ODT / IV', notes: 'First-line. Max single 8 mg. Repeat q8h. QT prolongation — avoid with other QT drugs. <6 mo: limited data.', weight_band_dose: { under_15kg: '2 mg', '15_to_30kg': '4 mg', '30kg_or_more': '8 mg' } }, + { name: 'Metoclopramide', dose_mg_per_kg: 0.15, max_mg: 10, unit: 'mg', route: 'IV / IM / PO', notes: '0.1-0.15 mg/kg (max 10 mg). Give with diphenhydramine to prevent EPS / dystonia.' }, + { name: 'Dimenhydrinate', dose_mg_per_kg: 1.25, max_mg: 50, unit: 'mg', route: 'PO / IV / IM / PR', notes: '1.25 mg/kg (max 50 mg) q6h. ≥2 yr.' }, + { name: 'Diphenhydramine', dose_mg_per_kg: 1, max_mg: 50, unit: 'mg', route: 'PO / IV / IM', notes: '1 mg/kg (max 50 mg) q6h. Adjunct, sedating.' }, + { name: 'Promethazine', dose_mg_per_kg: 0.25, max_mg: 25, unit: 'mg', route: 'PO / IV / IM', notes: '0.25-1 mg/kg. CONTRAINDICATED <2 yr (resp depression). Tissue injury if IV extrav.' }, + { name: 'Dexamethasone', dose_mg_per_kg: 0.15, max_mg: 10, unit: 'mg', route: 'IV / PO', notes: 'Adjunct, esp. chemo-induced. 0.15 mg/kg (max 10 mg).' }, + { name: 'Scopolamine patch', dose_mg_per_kg: null, max_mg: null, unit: 'mg', dose_display: '1.5 mg patch', route: 'Transdermal', notes: '≥12 yr. Motion sickness. Apply 4h before exposure.' } +]; + +// Prefer JSON-backed data if loaded; fall back to the inline list above. +function emetDrugs() { + var s = window._DRUGS && window._DRUGS.sections && window._DRUGS.sections.antiemetics; + return (s && s.drugs && s.drugs.length) ? s.drugs : EMET_FALLBACK; +} + +// Iterate the drug list to produce S.drugRow(...) output, handling the +// special cases (Ondansetron weight-band prefix, Scopolamine fixed dose). +function emetRows(wt, drugs) { + return drugs.map(function(dg) { + var dose; + if (dg.name === 'Ondansetron' && dg.weight_band_dose) { + var bd = dg.weight_band_dose; + var band = wt < 15 ? bd.under_15kg : wt < 30 ? bd['15_to_30kg'] : bd['30kg_or_more']; + dose = band + ' (weight band) OR ' + S.dStr(wt, dg.dose_mg_per_kg, dg.max_mg, ' ' + dg.unit) + ' (' + dg.dose_mg_per_kg + ' ' + dg.unit + '/kg)'; + } else if (dg.dose_display) { + dose = dg.dose_display; + } else { + dose = S.dStr(wt, dg.dose_mg_per_kg, dg.max_mg, ' ' + dg.unit); + } + return S.drugRow(dg.name, dose, dg.route, dg.notes); + }).join(''); +} + +function calcEmet() { + var wt = parseFloat(document.getElementById('emet-weight').value); + var el = document.getElementById('emet-result'); + if (!wt) { el.innerHTML = '

Enter weight (kg).

'; return; } + var html = '
Antiemetic doses — ' + wt + ' kg
'; + html += S.drugTable(emetRows(wt, emetDrugs())); + html += '
Pearls: Ondansetron is first-line in ED for acute gastroenteritis vomiting — single PO/ODT dose increases oral rehydration success. Avoid anticholinergics + opioid combinations. Check QTc if stacking ondansetron + other QT drugs.
'; + html += S.ref('AAP gastroenteritis guidance; WHO essential medicines; Lexicomp.'); + el.innerHTML = html; +} + +export function init() { + document.addEventListener('click', function(e) { + if (e.target.id === 'btn-emet-calc' || e.target.closest('#btn-emet-calc')) calcEmet(); + }); +} diff --git a/public/js/bedside/antimicrobials.js b/public/js/bedside/antimicrobials.js new file mode 100644 index 0000000..e2459a1 --- /dev/null +++ b/public/js/bedside/antimicrobials.js @@ -0,0 +1,65 @@ +// ============================================================ +// bedside/antimicrobials.js +// ANTIMICROBIALS (empiric, by age + infection type). +// ============================================================ + +import { S } from './shared.js'; + +var REG = { + neonate: { + sepsis: { first: 'Ampicillin + gentamicin', alt: 'Add cefotaxime if meningitis suspected or gram-neg concern', duration: '7-10 days; 21 days if confirmed meningitis', notes: 'Cover GBS, E. coli, Listeria. Add acyclovir if HSV risk (maternal lesions, vesicles, seizures).' }, + meningitis: { first: 'Ampicillin + cefotaxime (or gentamicin) + acyclovir', alt: 'Add vancomycin if gram-positive cocci', duration: '14-21 days depending on organism', notes: 'HSV coverage essential. LP + HSV PCR.' }, + pna: { first: 'Ampicillin + gentamicin', alt: 'Add cefotaxime for gram-neg', duration: '7-10 days', notes: 'Same as sepsis coverage.' }, + uti: { first: 'Ampicillin + gentamicin', alt: 'Cefotaxime if sensitivities known', duration: '10-14 days', notes: 'Evaluate for sepsis. Renal US and VCUG workup.' }, + skin: { first: 'Ampicillin + gentamicin', alt: 'Add vancomycin if MRSA risk', duration: '7-10 days', notes: 'Omphalitis, mastitis — broad coverage.' }, + ent: { first: 'Discuss with infectious diseases', alt: '—', duration: '—', notes: 'Rare in neonates. Any ear/throat infection warrants sepsis eval.' }, + neutropenic: { first: 'Cefepime or piperacillin-tazobactam', alt: 'Add vancomycin if indwelling line, severe mucositis, or MRSA', duration: 'Until ANC recovery + afebrile ≥48h', notes: 'Oncology/ID consult.' }, + ic: { first: 'Ampicillin + gentamicin + metronidazole', alt: 'Or piperacillin-tazobactam', duration: '7-14 days', notes: 'NEC: add metronidazole/clindamycin for anaerobes.' }, + bone: { first: 'Ampicillin + gentamicin', alt: 'Vancomycin if MRSA risk', duration: '3-6 weeks (IV then PO)', notes: 'Usually hematogenous. S. aureus, GBS, E. coli.' } + }, + infant: { + sepsis: { first: 'Ampicillin + ceftriaxone (or gentamicin)', alt: '+ vancomycin if MRSA / severe', duration: '7-10 days', notes: 'Add acyclovir if HSV suspected (<6 wks). Fever in <90 days requires workup.' }, + meningitis: { first: 'Ceftriaxone + vancomycin (± ampicillin if <6 wks for Listeria)', alt: '+ acyclovir if HSV', duration: '10-14 days (longer for Listeria/gram-neg)', notes: 'Dexamethasone if H. influenzae suspected.' }, + pna: { first: 'Ampicillin (or ceftriaxone)', alt: 'Add azithromycin if atypical', duration: '7-10 days', notes: 'S. pneumoniae most common. RSV/viral often primary.' }, + uti: { first: 'Ceftriaxone or cefotaxime', alt: 'Ampicillin + gentamicin', duration: '10-14 days', notes: 'US kidney/bladder if first febrile UTI.' }, + skin: { first: 'Cefazolin or clindamycin', alt: 'Vancomycin if MRSA risk', duration: '7-10 days', notes: 'Consider MRSA if purulent or local resistance >10%.' }, + ent: { first: 'Amoxicillin (90 mg/kg/day) for AOM', alt: 'Amox-clav if recent abx/treatment failure', duration: 'AOM 10 days <2 yr; 7 days ≥2 yr', notes: 'Observe mild AOM if >6 mo and no severe features.' }, + neutropenic: { first: 'Cefepime or piperacillin-tazobactam', alt: '+ vancomycin for severe/mucositis/line', duration: 'Until ANC recovery + afebrile', notes: 'Oncology/ID consult.' }, + ic: { first: 'Ceftriaxone + metronidazole', alt: 'Piperacillin-tazobactam', duration: '5-7 days (uncomplicated), longer for complicated', notes: 'Appendicitis — surgical consult.' }, + bone: { first: 'Cefazolin ± vancomycin', alt: 'Clindamycin', duration: '3-4 weeks (IV then PO transition)', notes: 'S. aureus most common. Kingella in <4 yr.' } + }, + child: { + sepsis: { first: 'Ceftriaxone + vancomycin', alt: 'Piperacillin-tazobactam if intra-abdominal', duration: '7-14 days', notes: 'Expand if specific source. Add antifungal if prolonged neutropenia.' }, + meningitis: { first: 'Ceftriaxone + vancomycin', alt: '+ dexamethasone (Hib coverage)', duration: '10-14 days', notes: 'Lumbar puncture. Dexamethasone 0.15 mg/kg q6h x 4 days.' }, + pna: { first: 'Ampicillin OR amoxicillin (high dose)', alt: 'Ceftriaxone if hospitalized; add azithromycin for atypical', duration: '5-7 days (uncomplicated)', notes: 'Mycoplasma if >5 yr. Consider viral causes.' }, + uti: { first: 'Cephalexin or TMP-SMX (PO)', alt: 'Ceftriaxone if ill', duration: '7-10 days (10-14 if pyelo)', notes: 'Culture-guided de-escalation.' }, + skin: { first: 'Cephalexin or clindamycin', alt: 'TMP-SMX or doxycycline if MRSA', duration: '5-10 days', notes: 'Abscess — incision & drainage primary tx.' }, + ent: { first: 'Amoxicillin 90 mg/kg/day (AOM); Penicillin V for strep pharyngitis', alt: 'Amox-clav; cephalexin if non-anaphylactic PCN allergy', duration: 'AOM 5-10 days; strep 10 days', notes: 'Strep pharyngitis dose: PCN V 250 mg BID-TID (<27 kg) or 500 mg BID (≥27 kg).' }, + neutropenic: { first: 'Cefepime OR piperacillin-tazobactam', alt: '+ vancomycin', duration: 'Until ANC recovery + afebrile', notes: 'Add empiric antifungal if fever >4-7 days.' }, + ic: { first: 'Ceftriaxone + metronidazole', alt: 'Piperacillin-tazobactam', duration: '5-7 days (uncomplicated appendicitis)', notes: 'Surgical consult.' }, + bone: { first: 'Cefazolin ± vancomycin', alt: 'Clindamycin', duration: '3-4 weeks total', notes: 'S. aureus, group A strep. Consider MRSA if severe.' } + } +}; + +function lookupAbx() { + var age = document.getElementById('abx-age').value; + var inf = document.getElementById('abx-infection').value; + var el = document.getElementById('abx-result'); + var r = REG[age] && REG[age][inf]; + if (!r) { el.innerHTML = '

No regimen found.

'; return; } + var ageLbl = { neonate: 'Neonate (0-28 d)', infant: 'Infant (1-3 mo)', child: 'Child (>3 mo)' }[age]; + var infLbl = document.querySelector('#abx-infection option[value="' + inf + '"]').textContent; + var html = '
' + ageLbl + ' — ' + infLbl + '
'; + html += '
First-line: ' + r.first + '
'; + html += '
Alternative / add: ' + r.alt + '
'; + html += '
Duration: ' + r.duration + '
'; + html += '
Notes: ' + r.notes + '
'; + html += S.ref('AAP Red Book, IDSA guidelines, Harriet Lane. Always tailor to local resistance patterns and culture results.'); + el.innerHTML = html; +} + +export function init() { + document.addEventListener('click', function(e) { + if (e.target.id === 'btn-abx-lookup' || e.target.closest('#btn-abx-lookup')) lookupAbx(); + }); +} diff --git a/public/js/bedside/burns.js b/public/js/bedside/burns.js new file mode 100644 index 0000000..24f8b1f --- /dev/null +++ b/public/js/bedside/burns.js @@ -0,0 +1,155 @@ +// ============================================================ +// 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 = '
'; + html += '
For each involved body region, enter % of that region burned (2° or deeper). Leave 0 if uninvolved. TBSA = sum of (region size × % involvement).
'; + html += '
'; + Object.keys(LB).forEach(function(key) { + var r = LB[key]; + var max = r.vals[idx]; + var isAgeDep = r.ageSensitive; + html += '
' + + '' + + ' %' + + '
'; + }); + html += '
'; + 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 = '

Enter weight (kg).

'; return; } + if (!tbsa || tbsa <= 0) { el.innerHTML = '

Enter % involvement per region, or override TBSA.

'; 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 = '
Burn fluid resuscitation — ' + wt + ' kg, ' + tbsa + '% TBSA (2° or deeper)
'; + html += '
Parkland formula: 4 mL × kg × %TBSA = ' + total + ' mL LR over 24 hours' + + '
  First 8 h (from time of burn): ' + first8 + ' mL (~' + rateFirst + ' mL/hr)' + + '
  Next 16 h: ' + next16 + ' mL (~' + rateNext + ' mL/hr)
'; + html += '
Plus maintenance fluids (4-2-1): ' + maint + ' mL/hr (D5 1/2NS ± 20 mEq KCl/L once UOP established). Consider dextrose in children <30 kg.
'; + html += '
Titrate to UOP: target 1-2 mL/kg/hr (infants / children), 0.5-1 mL/kg/hr (adolescents). Clinical response trumps formula.
'; + + // 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 += '
' + LB[key].label + ': ' + pct + '% of ' + LB[key].vals[idx] + '% = ' + contrib + '%
'; + } + }); + if (breakdown) { + html += '
Region breakdown
' + breakdown + '
'; + } + + html += '
Other pearls
'; + html += '
'; + html += 'Rule of palm: Patient\'s palm + fingers ≈ 1% TBSA — good for scattered burns.
'; + html += 'First-degree burns DO NOT count toward TBSA or Parkland.
'; + html += 'Analgesia: Morphine 0.05-0.1 mg/kg IV q2h, or fentanyl 1-2 mcg/kg IV q30-60 min.
'; + html += 'Tetanus prophylaxis if indicated. Tdap/Td ± tetanus immunoglobulin.'; + html += '
'; + + html += '
Burn center referral (ABA)
'; + html += '
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.
'; + 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); +} diff --git a/public/js/bedside/cardiac.js b/public/js/bedside/cardiac.js new file mode 100644 index 0000000..ae4c4f5 --- /dev/null +++ b/public/js/bedside/cardiac.js @@ -0,0 +1,103 @@ +// ============================================================ +// bedside/cardiac.js +// CARDIAC ARREST / PALS. +// ============================================================ + +import { S } from './shared.js'; + +function getWt() { + return parseFloat(document.getElementById('cardiac-weight').value) || 0; +} + +function title(k) { + return { general: 'PALS General Doses', asystole: 'Asystole / PEA', brady: 'Bradycardia', svt: 'SVT', vfib: 'VF / Pulseless VT', vt: 'Stable VT' }[k] || k; +} + +function cardiacShow(kind) { + var wt = getWt(); + var el = document.getElementById('cardiac-result'); + if (!wt) { el.innerHTML = '

Enter weight (kg).

'; return; } + var html = '
' + title(kind) + ' — ' + wt + ' kg
'; + + if (kind === 'general') { + html += S.drugTable( + S.drugRow('Epinephrine 1:10,000', S.dStr(wt, 0.01, 1, ' mg') + ' (' + S.dStr(wt, 0.1, 10, ' mL') + ')', 'IV / IO', '0.01 mg/kg = 0.1 mL/kg of 1:10,000. Max single dose 1 mg. Repeat q3-5 min.') + + S.drugRow('Epinephrine ET', S.dStr(wt, 0.1, 2.5, ' mg'), 'ETT', '0.1 mg/kg (1 mL/kg of 1:10,000) if no IV/IO') + + S.drugRow('Amiodarone', S.dStr(wt, 5, 300, ' mg'), 'IV / IO bolus', '5 mg/kg for arrest. Max 300 mg. Repeat up to 2 times (max 15 mg/kg/day).') + + S.drugRow('Lidocaine', S.dStr(wt, 1, 100, ' mg'), 'IV / IO bolus', '1 mg/kg. Alternative to amiodarone for VF/VT.') + + S.drugRow('Atropine', S.dStr(wt, 0.02, 0.5, ' mg') + ' (min 0.1 mg)', 'IV / IO', '0.02 mg/kg. Bradycardia from ↑vagal tone or AV block.') + + S.drugRow('Adenosine (1st)', S.dStr(wt, 0.1, 6, ' mg'), 'IV push + flush', '0.1 mg/kg (max 6 mg). For SVT. Rapid push, double flush.') + + S.drugRow('Adenosine (2nd)', S.dStr(wt, 0.2, 12, ' mg'), 'IV push + flush', '0.2 mg/kg (max 12 mg). Second dose if first unsuccessful.') + + S.drugRow('Sodium bicarb 8.4%', S.dStr(wt, 1, 50, ' mEq'), 'IV / IO', '1 mEq/kg. ONLY if severe metabolic acidosis, hyperK, or TCA OD. Not routine.') + + S.drugRow('Calcium chloride 10%', S.dStr(wt, 20, 1000, ' mg') + ' (' + S.dStr(wt, 0.2, 10, ' mL') + ')', 'IV / IO (central preferred)', 'For hyperK, hypoCa, Mg OD, CCB OD. 20 mg/kg = 0.2 mL/kg.') + + S.drugRow('Calcium gluconate 10%', S.dStr(wt, 60, 3000, ' mg') + ' (' + S.dStr(wt, 0.6, 30, ' mL') + ')', 'IV / IO (peripheral OK)', '60 mg/kg = 0.6 mL/kg. Preferred peripherally.') + + S.drugRow('Magnesium sulfate', S.dStr(wt, 50, 2000, ' mg'), 'IV over 10-20 min', '25-50 mg/kg. Torsades, severe asthma. Max 2 g.') + + S.drugRow('Dextrose 10%', S.dStr(wt, 2, null, ' mL') + ' (0.2 g/kg)', 'IV push', 'For documented hypoglycemia') + + S.drugRow('Defibrillation', '2 J/kg → 4 J/kg → 10 J/kg (max 10 J/kg or adult dose)', 'Pad', 'For VF/pulseless VT. Resume CPR immediately after shock.') + + S.drugRow('Cardioversion (sync)', '0.5-1 J/kg → 2 J/kg', 'Pad', 'For unstable SVT/VT. Sedate if possible.') + ); + html += '
Concentrations: Epi 1:10,000 (0.1 mg/mL) for IV arrest. 1:1000 (1 mg/mL) for IM anaphylaxis. Atropine <0.1 mg can cause paradoxical bradycardia.
'; + } else if (kind === 'asystole') { + html += S.drugTable( + S.drugRow('CPR', '100-120/min, depth 1/3 AP', '—', '15:2 (2 rescuer) or 30:2 (single). Rotate every 2 min.') + + S.drugRow('Epinephrine', S.dStr(wt, 0.01, 1, ' mg') + ' IV/IO', 'IV / IO', '0.01 mg/kg 1:10,000 = 0.1 mL/kg. Q3-5 min. Start early.') + + S.drugRow('Epinephrine ETT', S.dStr(wt, 0.1, 2.5, ' mg'), 'ETT', '0.1 mg/kg if no IV access') + ); + html += '
Reversible causes (H\'s & T\'s)
'; + html += '
'; + html += '
H\'s
Hypoxia, Hypovolemia, H+ (acidosis), Hypo/hyperK, Hypoglycemia, Hypothermia
'; + html += '
T\'s
Tension PTX, Tamponade, Toxins, Thrombosis (MI/PE), Trauma
'; + html += '
'; + } else if (kind === 'brady') { + html += S.stepBox('#f59e0b', 'Bradycardia with poor perfusion (HR < 60)', 'Start CPR if HR < 60 with poor perfusion despite oxygenation and ventilation.'); + html += S.arrow; + html += S.drugTable( + S.drugRow('Epinephrine', S.dStr(wt, 0.01, 1, ' mg'), 'IV / IO', 'First-line. Q3-5 min.') + + S.drugRow('Atropine', S.dStr(wt, 0.02, 0.5, ' mg') + ' (min 0.1 mg)', 'IV / IO', 'If ↑ vagal tone or AV block. Max 1 mg child / 0.5 mg infant.') + + S.drugRow('Transcutaneous pacing', '—', 'Pad', 'For refractory bradycardia not responsive to drugs.') + + S.drugRow('Consider', '—', '—', 'Hypoxia, tension pneumothorax, ↑ICP, toxic ingestion (organo, CCB, BB), heart block') + ); + } else if (kind === 'svt') { + html += S.stepBox('#3b82f6', 'SVT — narrow complex, rate usually >220 infant / >180 child', 'Differentiate from sinus tach: abrupt onset/offset, no P waves or abnormal P axis, HR minimally variable.'); + html += S.arrow; + html += '
If STABLE
'; + html += S.drugTable( + S.drugRow('Vagal maneuvers', 'Ice to face, blow into syringe, Valsalva', '—', 'Try first in stable patient') + + S.drugRow('Adenosine (1st)', S.dStr(wt, 0.1, 6, ' mg'), 'IV push (proximal) + flush', 'Rapid push, 3-way stopcock with NS flush') + + S.drugRow('Adenosine (2nd)', S.dStr(wt, 0.2, 12, ' mg'), 'IV push + flush', 'If first dose unsuccessful') + + S.drugRow('Procainamide', '15 mg/kg over 30-60 min', 'IV', 'Expert consult. Avoid with amiodarone.') + + S.drugRow('Amiodarone', '5 mg/kg over 20-60 min', 'IV', 'Expert consult.') + ); + html += '
If UNSTABLE
'; + html += S.drugTable(S.drugRow('Synchronized cardioversion', '0.5-1 J/kg → 2 J/kg', 'Pad', 'Sedate if possible. Pre-treat with adenosine if IV available.')); + } else if (kind === 'vfib') { + html += S.stepBox('#dc2626', 'VF / Pulseless VT', 'High-quality CPR + defibrillation are key. Minimize interruptions.'); + html += S.arrow; + html += S.drugTable( + S.drugRow('Defibrillation (1st)', '2 J/kg', 'Pad', 'Resume CPR immediately after shock.') + + S.drugRow('Defibrillation (2nd)', '4 J/kg', 'Pad', 'After 2 min CPR.') + + S.drugRow('Defibrillation (3rd+)', '≥4 J/kg (max 10 J/kg or adult dose)', 'Pad', '') + + S.drugRow('Epinephrine', S.dStr(wt, 0.01, 1, ' mg'), 'IV / IO', 'After 2nd shock. Q3-5 min.') + + S.drugRow('Amiodarone', S.dStr(wt, 5, 300, ' mg'), 'IV / IO bolus', 'Refractory VF/VT. Max 15 mg/kg/day.') + + S.drugRow('Lidocaine', S.dStr(wt, 1, 100, ' mg'), 'IV / IO', 'Alternative to amiodarone. Then 20-50 mcg/kg/min gtt.') + + S.drugRow('Mg sulfate', S.dStr(wt, 50, 2000, ' mg'), 'IV over 1-2 min', 'For torsades or hypoMg.') + ); + } else if (kind === 'vt') { + html += S.stepBox('#f59e0b', 'Stable monomorphic VT', 'Expert consult. Avoid combining QT-prolonging agents.'); + html += S.arrow; + html += S.drugTable( + S.drugRow('Amiodarone', '5 mg/kg over 20-60 min', 'IV', '= ' + S.d(wt, 5, 300) + ' mg over 20-60 min') + + S.drugRow('Procainamide', '15 mg/kg over 30-60 min', 'IV', 'Do not combine with amiodarone') + + S.drugRow('Sync cardioversion', '0.5-1 → 2 J/kg', 'Pad', 'If becomes unstable. Sedate.') + ); + } + html += S.ref('AHA PALS Guidelines 2020. Always verify against institutional protocols.'); + el.innerHTML = html; +} + +export function init() { + document.addEventListener('click', function(e) { + var btn = e.target.closest('[data-cardiac]'); + if (btn) cardiacShow(btn.dataset.cardiac); + }); +} diff --git a/public/js/bedside/image-lightbox.js b/public/js/bedside/image-lightbox.js new file mode 100644 index 0000000..8005d24 --- /dev/null +++ b/public/js/bedside/image-lightbox.js @@ -0,0 +1,40 @@ +// ============================================================ +// bedside/image-lightbox.js +// IMAGE LIGHTBOX — any element with data-img-src opens fullscreen. +// ============================================================ + +export function init() { + document.addEventListener('click', function(e) { + var opener = e.target.closest && e.target.closest('[data-img-src]'); + if (opener) { + e.preventDefault(); + var lb = document.getElementById('img-lightbox'); + if (!lb) return; + var src = opener.dataset.imgSrc; + var title = opener.dataset.imgTitle || ''; + document.getElementById('img-lightbox-img').src = src; + document.getElementById('img-lightbox-img').alt = title; + document.getElementById('img-lightbox-title').textContent = title; + document.getElementById('img-lightbox-open').href = src; + lb.style.display = 'flex'; + return; + } + // Close on close button, on backdrop click (not on image), and on Escape (below) + var closeBtn = e.target.closest && e.target.closest('#img-lightbox-close'); + var lb2 = document.getElementById('img-lightbox'); + if (!lb2 || lb2.style.display !== 'flex') return; + if (closeBtn || e.target === lb2) { + lb2.style.display = 'none'; + document.getElementById('img-lightbox-img').src = ''; + } + }); + document.addEventListener('keydown', function(e) { + if (e.key === 'Escape') { + var lb = document.getElementById('img-lightbox'); + if (lb && lb.style.display === 'flex') { + lb.style.display = 'none'; + document.getElementById('img-lightbox-img').src = ''; + } + } + }); +} diff --git a/public/js/bedside/index.js b/public/js/bedside/index.js new file mode 100644 index 0000000..8327170 --- /dev/null +++ b/public/js/bedside/index.js @@ -0,0 +1,46 @@ +// ============================================================ +// bedside/index.js +// Imports + registers all Bedside section modules on load. +// Loaded as