From 54285865d5e509ef2c3ec04200513b2e77fde574 Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 20 Apr 2026 04:23:18 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20B=20=E2=80=94=20extract=20drug=20data?= =?UTF-8?q?=20to=20public/data/drugs.json=20(schema=20v1.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moved 43 weight-based drug entries out of calculators.js string literals into a structured JSON reference. Renderers now iterate JSON → S.drugRow. Sections extracted (43 drugs): - anaphylaxis (7), sedation (11), agitation (11), antiemetics (7), seizure (7) Out of scope: seizure refractory drips (compound strings), NRP, antimicrobial empiric regimens (already structured), airway/cardiac/tox/trauma/respiratory /sepsis/burns sections (fine as-is or no drugs). New: - public/data/drugs.json — { version, last_reviewed, sections..drugs[] } - public/js/drugs-loader.js — fetches JSON on boot, exposes window._DRUGS + window._DRUGS_READY Promise. Non-fatal: each calc function has a matching *_FALLBACK constant so a 404 on drugs.json doesn't break anything. Schema: - dose_mg_per_kg | dose_mg_per_kg_low/high (for ranges) | max_mg | unit | route | notes | source | optional special-cases (weight bands, age-dep text) Added one unit test asserting drugs.json loads + has all 5 sections with non-empty drugs arrays. 37/37 unit tests + 26/26 Playwright e2e tests pass. --- .gitignore | 1 + public/data/drugs.json | 498 ++++++++++++++++++++++++++++++++++++++ public/e2e-harness.html | 1 + public/index.html | 1 + public/js/calculators.js | 316 +++++++++++++++++++----- public/js/drugs-loader.js | 31 +++ test/calc-math.test.js | 17 ++ 7 files changed, 802 insertions(+), 63 deletions(-) create mode 100644 public/data/drugs.json create mode 100644 public/js/drugs-loader.js diff --git a/.gitignore b/.gitignore index 723bf9f..e224646 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ node_modules/ .env.local .env.production data/ +!public/data/ *.db *.db-journal *.db-wal diff --git a/public/data/drugs.json b/public/data/drugs.json new file mode 100644 index 0000000..0c888ed --- /dev/null +++ b/public/data/drugs.json @@ -0,0 +1,498 @@ +{ + "version": "1.0", + "last_reviewed": "2026-04-20", + "sections": { + "anaphylaxis": { + "drugs": [ + { + "name": "Epinephrine IM", + "dose_mg_per_kg": 0.01, + "max_mg": 0.5, + "unit": "mg", + "route": "IM", + "notes": "1:1000 (1 mg/mL), lateral thigh. May repeat q5-15 min.", + "concentration": "1:1000", + "source": "ASCIA 2021" + }, + { + "name": "Normal saline bolus", + "dose_mg_per_kg": 20, + "max_mg": null, + "unit": "mL", + "route": "IV/IO", + "notes": "Repeat up to 60 mL/kg for hypotension", + "source": "ASCIA 2021" + }, + { + "name": "Diphenhydramine", + "dose_mg_per_kg": 1.25, + "max_mg": 50, + "unit": "mg", + "route": "IV/IM/PO", + "notes": "H1 blocker. NOT first-line — adjunct only", + "source": "AAP/ACAAI" + }, + { + "name": "Ranitidine", + "dose_mg_per_kg": 1, + "max_mg": 50, + "unit": "mg", + "route": "IV over 5 min", + "notes": "H2 blocker. Optional adjunct", + "source": "AAP/ACAAI" + }, + { + "name": "Dexamethasone", + "dose_mg_per_kg": 0.6, + "max_mg": 16, + "unit": "mg", + "route": "IV/IM/PO", + "notes": "Prevents biphasic reaction (4-6 hrs later)", + "source": "AAP/ACAAI" + }, + { + "name": "Methylprednisolone", + "dose_mg_per_kg": 2, + "max_mg": 125, + "unit": "mg", + "route": "IV", + "notes": "Alternative steroid", + "source": "AAP/ACAAI" + }, + { + "name": "Glucagon", + "dose_mg_per_kg": 0.02, + "max_mg": 1, + "unit": "mg", + "route": "IV/IM", + "notes": "For patients on beta-blockers not responding to epi", + "source": "ASCIA 2021" + } + ] + }, + "sedation": { + "drugs": [ + { + "name": "Ketamine IV", + "display": "Ketamine", + "dose_mg_per_kg_low": 1.5, + "dose_mg_per_kg_high": 2, + "max_mg_low": 100, + "max_mg_high": 150, + "unit": "mg", + "route": "IV", + "onset": "1 min", + "duration": "15-30 min", + "notes": "Dissociative. Preserves airway reflexes.", + "source": "AAP sedation guideline" + }, + { + "name": "Ketamine IM", + "display": "", + "dose_mg_per_kg_low": 4, + "dose_mg_per_kg_high": 5, + "max_mg_low": 300, + "max_mg_high": 400, + "unit": "mg", + "route": "IM", + "onset": "3-5 min", + "duration": "30-60 min", + "notes": "Give atropine 0.01 mg/kg to reduce secretions.", + "source": "AAP sedation guideline" + }, + { + "name": "Midazolam IV", + "display": "Midazolam", + "dose_mg_per_kg_low": 0.05, + "dose_mg_per_kg_high": 0.1, + "max_mg_low": 2, + "max_mg_high": 5, + "unit": "mg", + "route": "IV", + "onset": "2-3 min", + "duration": "30-60 min", + "notes": "Anxiolysis. Titrate q3-5 min.", + "source": "AAP sedation guideline" + }, + { + "name": "Midazolam IN/PO", + "display": "", + "dose_mg_per_kg": 0.5, + "max_mg": 20, + "unit": "mg", + "route": "IN/PO", + "onset": "10-15 min", + "duration": "30-60 min", + "notes": "IN (max 10 mg / naris) or PO (max 20 mg).", + "source": "AAP sedation guideline" + }, + { + "name": "Propofol", + "display": "Propofol", + "dose_mg_per_kg": 1, + "max_mg": 40, + "unit": "mg", + "route": "IV", + "onset": "30 sec", + "duration": "5-10 min", + "notes": "Short procedures. Causes apnea — manage airway.", + "source": "AAP sedation guideline" + }, + { + "name": "Fentanyl IV", + "display": "Fentanyl", + "dose_mg_per_kg": 1, + "max_mg": 100, + "unit": "mcg", + "route": "IV", + "onset": "2-3 min", + "duration": "30-60 min", + "notes": "Analgesic. Often combined with midazolam.", + "source": "AAP sedation guideline" + }, + { + "name": "Fentanyl IN", + "display": "", + "dose_mg_per_kg": 2, + "max_mg": 100, + "unit": "mcg", + "route": "IN", + "onset": "5-10 min", + "duration": "30-60 min", + "notes": "Intranasal.", + "source": "AAP sedation guideline" + }, + { + "name": "Nitrous oxide", + "display": "Nitrous oxide", + "dose_mg_per_kg": null, + "max_mg": null, + "unit": "mix", + "dose_display": "50:50 or 70:30 mix", + "route": "Inhaled", + "onset": "2-5 min", + "duration": "5 min off", + "notes": "Self-administered via demand valve. Anxiolysis + mild analgesia.", + "source": "AAP sedation guideline" + }, + { + "name": "Dexmedetomidine IN", + "display": "Dexmedetomidine", + "dose_mg_per_kg_low": 2, + "dose_mg_per_kg_high": 3, + "max_mg_low": 100, + "max_mg_high": null, + "unit": "mcg", + "route": "IN", + "onset": "15-30 min", + "duration": "60-90 min", + "notes": "Intranasal. No respiratory depression. Good for imaging.", + "source": "AAP sedation guideline" + }, + { + "name": "Naloxone", + "display": "Naloxone", + "dose_mg_per_kg": 0.1, + "max_mg": 2, + "unit": "mg", + "route": "IV/IM/IN", + "reverses": "Opioids (fentanyl, morphine)", + "notes": "Max 2 mg. Repeat q2-3 min. Duration shorter than opioids — monitor for re-sedation.", + "source": "AAP sedation guideline" + }, + { + "name": "Flumazenil", + "display": "Flumazenil", + "dose_mg_per_kg": 0.01, + "max_mg": 0.2, + "unit": "mg", + "route": "IV", + "reverses": "Benzodiazepines (midazolam)", + "notes": "Max 0.2 mg single dose. Repeat q1 min to max 1 mg total. Risk of seizures — use cautiously.", + "source": "AAP sedation guideline" + } + ] + }, + "agitation": { + "drugs": [ + { + "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.", + "source": "AAP/ACEP agitation" + }, + { + "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)", + "source": "AAP/ACEP agitation" + }, + { + "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)", + "source": "AAP/ACEP agitation" + }, + { + "name": "Olanzapine (ODT)", + "display": "Olanzapine (ODT)", + "step": 2, + "dose_mg_per_kg": null, + "max_mg": null, + "unit": "mg", + "route": "PO / ODT", + "dose_display_if_under_30kg": "2.5-5 mg", + "dose_display_if_30kg_or_more": "5-10 mg", + "notes": "Age ≥6 yr. Avoid IM + benzo combo (risk of resp depression).", + "source": "AAP/ACEP agitation" + }, + { + "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.", + "source": "AAP/ACEP agitation" + }, + { + "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)", + "source": "AAP/ACEP agitation" + }, + { + "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.", + "source": "AAP/ACEP agitation" + }, + { + "name": "Haloperidol", + "display": "Haloperidol", + "step": 3, + "dose_mg_per_kg": 0.05, + "max_mg": null, + "unit": "mg", + "route": "IM / IV", + "dose_display_if_under_40kg": "0.025-0.075 mg/kg", + "dose_display_if_40kg_or_more": "2.5-5 mg", + "notes": "Avoid <3 yr. Risk: QT, EPS, NMS. ECG if repeated.", + "source": "AAP/ACEP agitation" + }, + { + "name": "Olanzapine IM", + "display": "Olanzapine", + "step": 3, + "dose_mg_per_kg": null, + "max_mg": null, + "unit": "mg", + "route": "IM", + "dose_display_if_under_40kg": "2.5-5 mg", + "dose_display_if_40kg_or_more": "5-10 mg", + "notes": "Avoid benzo co-administration (resp depression, hypotension)", + "source": "AAP/ACEP agitation" + }, + { + "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.", + "source": "AAP/ACEP agitation" + }, + { + "name": "Droperidol", + "display": "Droperidol", + "step": 3, + "dose_mg_per_kg": 0.05, + "max_mg": null, + "unit": "mg", + "route": "IM / IV", + "dose_display_prefix": "0.03-0.07 mg/kg", + "notes": "Effective but QT concern — get ECG.", + "source": "AAP/ACEP agitation" + } + ] + }, + "antiemetics": { + "drugs": [ + { + "name": "Ondansetron", + "dose_mg_per_kg": 0.15, + "max_mg": 8, + "unit": "mg", + "route": "PO / ODT / IV", + "weight_band_dose": { + "under_15kg": "2 mg", + "15_to_30kg": "4 mg", + "30kg_or_more": "8 mg" + }, + "notes": "First-line. Max single 8 mg. Repeat q8h. QT prolongation — avoid with other QT drugs. <6 mo: limited data.", + "source": "AAP gastroenteritis; Lexicomp" + }, + { + "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.", + "source": "Lexicomp" + }, + { + "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.", + "source": "Lexicomp" + }, + { + "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.", + "source": "Lexicomp" + }, + { + "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.", + "source": "Lexicomp" + }, + { + "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).", + "source": "Lexicomp" + }, + { + "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.", + "source": "Lexicomp" + } + ] + }, + "seizure": { + "drugs": [ + { + "name": "Lorazepam", + "phase": "1st benzo", + "dose_mg_per_kg": 0.1, + "max_mg": 4, + "unit": "mg", + "route": "IV / IO", + "label_suffix": " (IV preferred)", + "notes": "Slow push over 1-2 min.", + "source": "AES 2016; ESETT 2019" + }, + { + "name": "Midazolam", + "phase": "1st benzo", + "dose_mg_per_kg": 0.2, + "max_mg": 10, + "unit": "mg", + "route": "IM / IN / buccal", + "notes": "Use 5 mg/mL concentrate for IN; split between nares.", + "source": "AES 2016; ESETT 2019" + }, + { + "name": "Diazepam", + "phase": "1st benzo", + "dose_mg_per_kg": 0.5, + "max_mg": 20, + "unit": "mg", + "route": "PR", + "notes": "Only if no IV/IM/IN access.", + "source": "AES 2016" + }, + { + "name": "Levetiracetam", + "phase": "2nd-line", + "dose_mg_per_kg": 60, + "max_mg": 4500, + "unit": "mg", + "route": "IV over 5-15 min", + "label_suffix": " (preferred)", + "notes": "Best tolerability. No ECG monitoring needed.", + "source": "ESETT 2019" + }, + { + "name": "Fosphenytoin", + "phase": "2nd-line", + "dose_mg_per_kg": 20, + "max_mg": 1500, + "unit": "mg PE", + "route": "IV over 10 min", + "notes": "Max rate 3 mg PE/kg/min. Monitor ECG, BP. Avoid if cardiac dysrhythmia.", + "source": "ESETT 2019" + }, + { + "name": "Valproic acid", + "phase": "2nd-line", + "dose_mg_per_kg": 40, + "max_mg": 3000, + "unit": "mg", + "route": "IV over 10 min", + "notes": "Avoid <2 yr, hepatic / mitochondrial disease, pregnancy.", + "source": "ESETT 2019" + }, + { + "name": "Phenobarbital", + "phase": "2nd-line", + "dose_mg_per_kg": 20, + "max_mg": 1000, + "unit": "mg", + "route": "IV over 20 min", + "notes": "Last-choice 2nd line. High risk of resp depression + hypotension.", + "source": "AES 2016" + } + ] + } + } +} diff --git a/public/e2e-harness.html b/public/e2e-harness.html index e1aa96b..d109f19 100644 --- a/public/e2e-harness.html +++ b/public/e2e-harness.html @@ -16,6 +16,7 @@
+ diff --git a/public/index.html b/public/index.html index b774db7..6d5807f 100644 --- a/public/index.html +++ b/public/index.html @@ -445,6 +445,7 @@ + diff --git a/public/js/calculators.js b/public/js/calculators.js index 9edd3f2..4933453 100644 --- a/public/js/calculators.js +++ b/public/js/calculators.js @@ -2565,6 +2565,44 @@ ''; } + // Fallback inline data — mirrors the non-refractory seizure entries in + // drugs.json. Refractory infusion drips are kept inline below because + // their dose strings (bolus → gtt range) don't map cleanly to the + // single-row JSON schema. + var SEIZURE_FALLBACK = [ + { name: 'Lorazepam', phase: '1st benzo', dose_mg_per_kg: 0.1, max_mg: 4, unit: 'mg', route: 'IV / IO', label_suffix: ' (IV preferred)', notes: 'Slow push over 1-2 min.' }, + { name: 'Midazolam', phase: '1st benzo', dose_mg_per_kg: 0.2, max_mg: 10, unit: 'mg', route: 'IM / IN / buccal', notes: 'Use 5 mg/mL concentrate for IN; split between nares.' }, + { name: 'Diazepam', phase: '1st benzo', dose_mg_per_kg: 0.5, max_mg: 20, unit: 'mg', route: 'PR', notes: 'Only if no IV/IM/IN access.' }, + { name: 'Levetiracetam', phase: '2nd-line', dose_mg_per_kg: 60, max_mg: 4500, unit: 'mg', route: 'IV over 5-15 min', label_suffix: ' (preferred)', notes: 'Best tolerability. No ECG monitoring needed.' }, + { name: 'Fosphenytoin', phase: '2nd-line', dose_mg_per_kg: 20, max_mg: 1500, unit: 'mg PE', route: 'IV over 10 min', notes: 'Max rate 3 mg PE/kg/min. Monitor ECG, BP. Avoid if cardiac dysrhythmia.' }, + { name: 'Valproic acid', phase: '2nd-line', dose_mg_per_kg: 40, max_mg: 3000, unit: 'mg', route: 'IV over 10 min', notes: 'Avoid <2 yr, hepatic / mitochondrial disease, pregnancy.' }, + { name: 'Phenobarbital', phase: '2nd-line', dose_mg_per_kg: 20, max_mg: 1000, unit: 'mg', route: 'IV over 20 min', notes: 'Last-choice 2nd line. High risk of resp depression + hypotension.' } + ]; + + function seizureDrugs() { + var s = window._DRUGS && window._DRUGS.sections && window._DRUGS.sections.seizure; + if (s && s.drugs && s.drugs.length) { + return s.drugs.map(function(d) { + var fb = SEIZURE_FALLBACK.filter(function(x) { return x.name === d.name; })[0] || {}; + return Object.assign({}, fb, d); + }); + } + return SEIZURE_FALLBACK; + } + + // Build the joined S.drugRow output for one phase ("1st benzo" / "2nd-line"). + function seizureRowsByPhase(wt, phase) { + var S = window._EM; + return seizureDrugs().filter(function(dg) { return dg.phase === phase; }).map(function(dg) { + var label = dg.name + (dg.label_suffix || ''); + var unitArg = dg.unit && dg.unit !== 'mg' ? ' ' + dg.unit : undefined; + var dose = unitArg + ? S.dStr(wt, dg.dose_mg_per_kg, dg.max_mg, unitArg) + : S.dStr(wt, dg.dose_mg_per_kg, dg.max_mg); + return S.drugRow(label, dose, dg.route, dg.notes); + }).join(''); + } + function calcSeizure() { var wt = parseFloat(document.getElementById('seizure-weight').value); var resultDiv = document.getElementById('seizure-result'); @@ -2585,11 +2623,7 @@ // 5 min — 1ST BENZO html += stepRow('5 min', '#8b5cf6', '1st benzodiazepine (give once, pick by access)', - S.drugTable( - S.drugRow('Lorazepam (IV preferred)', S.dStr(wt, 0.1, 4), 'IV / IO', 'Slow push over 1-2 min.') + - S.drugRow('Midazolam', S.dStr(wt, 0.2, 10), 'IM / IN / buccal', 'Use 5 mg/mL concentrate for IN; split between nares.') + - S.drugRow('Diazepam', S.dStr(wt, 0.5, 20), 'PR', 'Only if no IV/IM/IN access.') - ) + S.drugTable(seizureRowsByPhase(wt, '1st benzo')) ); // 10 min — 2ND BENZO @@ -2599,12 +2633,7 @@ // 20 min — 2ND-LINE html += stepRow('20 min', '#f59e0b', '2nd-line anti-epileptic — pick one (ESETT: equivalent efficacy)', - S.drugTable( - S.drugRow('Levetiracetam (preferred)', S.dStr(wt, 60, 4500), 'IV over 5-15 min', 'Best tolerability. No ECG monitoring needed.') + - S.drugRow('Fosphenytoin', S.dStr(wt, 20, 1500, ' mg PE'), 'IV over 10 min', 'Max rate 3 mg PE/kg/min. Monitor ECG, BP. Avoid if cardiac dysrhythmia.') + - S.drugRow('Valproic acid', S.dStr(wt, 40, 3000), 'IV over 10 min', 'Avoid <2 yr, hepatic / mitochondrial disease, pregnancy.') + - S.drugRow('Phenobarbital', S.dStr(wt, 20, 1000), 'IV over 20 min', 'Last-choice 2nd line. High risk of resp depression + hypotension.') - ) + S.drugTable(seizureRowsByPhase(wt, '2nd-line')) ); // 30 min — 2nd of 2nd-line (optional) @@ -2646,6 +2675,28 @@ 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').value; @@ -2653,14 +2704,22 @@ if (!wt) { resultDiv.innerHTML = '

Enter weight (kg).

'; return; } var S = window._EM; - var epiDose = d(wt, 0.01, 0.5); + 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 += '
0.01 mg/kg (max 0.5 mg) | Auto-injector: ' + autoInjector + '
'; + 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 += '
'; @@ -2668,14 +2727,14 @@ html += 'STEP 2PositionSupine + legs elevated—If dyspneic: sitting position. If vomiting: recovery position'; html += 'STEP 3O2High flow 10-15 L/minFace mask100% O2. Prepare for airway management'; - html += 'STEP 4IV fluidsNS ' + S.dStr(wt, 20, null, ' mL') + ' bolusIV/IORepeat up to 60 mL/kg for hypotension'; - html += 'STEP 5Diphenhydramine' + S.dStr(wt, 1.25, 50) + 'IV/IM/POH1 blocker. NOT first-line — adjunct only'; - html += 'Ranitidine' + S.dStr(wt, 1, 50) + 'IV over 5 minH2 blocker. Optional adjunct'; - html += 'STEP 6Dexamethasone' + S.dStr(wt, 0.6, 16) + 'IV/IM/POPrevents biphasic reaction (4-6 hrs later)'; - html += 'Methylprednisolone' + S.dStr(wt, 2, 125) + 'IVAlternative steroid'; + html += 'STEP 4IV fluidsNS ' + S.dStr(wt, fluids.dose_mg_per_kg, fluids.max_mg, ' mL') + ' bolusIV/IORepeat up to 60 mL/kg for hypotension'; + html += 'STEP 5Diphenhydramine' + S.dStr(wt, dph.dose_mg_per_kg, dph.max_mg) + 'IV/IM/POH1 blocker. NOT first-line — adjunct only'; + html += 'Ranitidine' + S.dStr(wt, rnt.dose_mg_per_kg, rnt.max_mg) + 'IV over 5 minH2 blocker. Optional adjunct'; + html += 'STEP 6Dexamethasone' + S.dStr(wt, dex.dose_mg_per_kg, dex.max_mg) + 'IV/IM/POPrevents biphasic reaction (4-6 hrs later)'; + html += 'Methylprednisolone' + S.dStr(wt, mpn.dose_mg_per_kg, mpn.max_mg) + 'IVAlternative steroid'; html += 'IF REFRACTORYEpinephrine gtt0.1-1 mcg/kg/minIV infusionFor persistent hypotension despite fluids + IM epi'; - html += 'Glucagon' + S.dStr(wt, 0.02, 1) + 'IV/IMFor patients on beta-blockers not responding to epi'; + html += 'Glucagon' + S.dStr(wt, glc.dose_mg_per_kg, glc.max_mg) + 'IV/IMFor patients on beta-blockers not responding to epi'; html += ''; 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.
'; @@ -2694,39 +2753,95 @@ function d(wt, mgPerKg, max) { var v = Math.round(wt * mgPerKg * 100) / 100; return max ? Math.min(v, max) : v; } + // Fallback inline data — mirrors drugs.json sedation section. The first + // table's rows group by drug (Ketamine has both IV + IM lines), hence the + // display vs name split. `reverses` marks the final 2 rows as reversal + // agents shown in a separate table. + var SED_FALLBACK = [ + { name: 'Ketamine IV', display: 'Ketamine', dose_mg_per_kg_low: 1.5, dose_mg_per_kg_high: 2, max_mg_low: 100, max_mg_high: 150, unit: 'mg', route: 'IV', onset: '1 min', duration: '15-30 min', notes: 'Dissociative. Preserves airway reflexes.' }, + { name: 'Ketamine IM', display: '', dose_mg_per_kg_low: 4, dose_mg_per_kg_high: 5, max_mg_low: 300, max_mg_high: 400, unit: 'mg', route: 'IM', onset: '3-5 min', duration: '30-60 min', notes: 'Give atropine 0.01 mg/kg to reduce secretions.' }, + { name: 'Midazolam IV', display: 'Midazolam', dose_mg_per_kg_low: 0.05, dose_mg_per_kg_high: 0.1, max_mg_low: 2, max_mg_high: 5, unit: 'mg', route: 'IV', onset: '2-3 min', duration: '30-60 min', notes: 'Anxiolysis. Titrate q3-5 min.' }, + { name: 'Midazolam IN/PO', display: '', dose_mg_per_kg: 0.5, max_mg: 20, unit: 'mg', route: 'IN/PO', onset: '10-15 min', duration: '30-60 min', notes: 'IN (max 10 mg / naris) or PO (max 20 mg).' }, + { name: 'Propofol', display: 'Propofol', dose_mg_per_kg: 1, max_mg: 40, unit: 'mg', route: 'IV', onset: '30 sec', duration: '5-10 min', notes: 'Short procedures. Causes apnea — manage airway.' }, + { name: 'Fentanyl IV', display: 'Fentanyl', dose_mg_per_kg: 1, max_mg: 100, unit: 'mcg', route: 'IV', onset: '2-3 min', duration: '30-60 min', notes: 'Analgesic. Often combined with midazolam.' }, + { name: 'Fentanyl IN', display: '', dose_mg_per_kg: 2, max_mg: 100, unit: 'mcg', route: 'IN', onset: '5-10 min', duration: '30-60 min', notes: 'Intranasal.' }, + { name: 'Nitrous oxide', display: 'Nitrous oxide', dose_display: '50:50 or 70:30 mix', unit: 'mix', route: 'Inhaled',onset: '2-5 min', duration: '5 min off', notes: 'Self-administered via demand valve. Anxiolysis + mild analgesia.' }, + { name: 'Dexmedetomidine IN', display: 'Dexmedetomidine', dose_mg_per_kg_low: 2, dose_mg_per_kg_high: 3, max_mg_low: 100, max_mg_high: null, unit: 'mcg', route: 'IN', onset: '15-30 min', duration: '60-90 min', notes: 'Intranasal. No respiratory depression. Good for imaging.' }, + { name: 'Naloxone', display: 'Naloxone', dose_mg_per_kg: 0.1, max_mg: 2, unit: 'mg', route: 'IV/IM/IN', reverses: 'Opioids (fentanyl, morphine)', notes: 'Max 2 mg. Repeat q2-3 min. Duration shorter than opioids — monitor for re-sedation.' }, + { name: 'Flumazenil', display: 'Flumazenil', dose_mg_per_kg: 0.01, max_mg: 0.2, unit: 'mg', route: 'IV', reverses: 'Benzodiazepines (midazolam)', notes: 'Max 0.2 mg single dose. Repeat q1 min to max 1 mg total. Risk of seizures — use cautiously.' } + ]; + + function sedDrugs() { + var s = window._DRUGS && window._DRUGS.sections && window._DRUGS.sections.sedation; + if (s && s.drugs && s.drugs.length) { + return s.drugs.map(function(d) { + var fb = SED_FALLBACK.filter(function(x) { return x.name === d.name; })[0] || {}; + return Object.assign({}, fb, d); + }); + } + return SED_FALLBACK; + } + + // perKg footer that matches the original inline format exactly. + function sedPerKg(lo, hi, unit) { + unit = unit || 'mg'; + return ' (' + lo + (hi != null ? '-' + hi : '') + ' ' + unit + '/kg)'; + } + + // Build the Dose cell text for a sedation drug (supports low/high range + // and single-dose entries; honors custom dose_display for fixed doses). + function sedDoseCell(wt, dg) { + if (dg.dose_display) return dg.dose_display; + var unitLabel = dg.unit === 'mcg' ? ' mcg' : (dg.unit === 'mix' ? '' : ' mg'); + var perKgUnit = dg.unit === 'mcg' ? 'mcg' : 'mg'; + if (dg.dose_mg_per_kg_low != null) { + var lo = d(wt, dg.dose_mg_per_kg_low, dg.max_mg_low); + if (dg.dose_mg_per_kg_high != null) { + var hi = d(wt, dg.dose_mg_per_kg_high, dg.max_mg_high); + return lo + '-' + hi + unitLabel + sedPerKg(dg.dose_mg_per_kg_low, dg.dose_mg_per_kg_high, perKgUnit); + } + return lo + unitLabel + sedPerKg(dg.dose_mg_per_kg_low, null, perKgUnit); + } + if (dg.dose_mg_per_kg != null) { + var v = d(wt, dg.dose_mg_per_kg, dg.max_mg); + return v + unitLabel + sedPerKg(dg.dose_mg_per_kg, null, perKgUnit); + } + return ''; + } + + function sedRow(wt, dg, showReverses) { + var displayName = dg.display == null ? dg.name : dg.display; + var nameCell = displayName + ? '' + displayName + '' + : ''; + var dose = sedDoseCell(wt, dg); + if (showReverses) { + return '' + nameCell + '' + dose + '' + dg.route + '' + (dg.reverses || '') + '' + dg.notes + ''; + } + return '' + nameCell + '' + dose + '' + dg.route + '' + (dg.onset || '') + '' + (dg.duration || '') + '' + dg.notes + ''; + } + function calcSedation() { var wt = parseFloat(document.getElementById('sed-weight').value); var resultDiv = document.getElementById('sed-result'); if (!wt) { resultDiv.innerHTML = '

Enter weight (kg).

'; return; } + var all = sedDrugs(); + var agents = all.filter(function(dg) { return !dg.reverses; }); + var reversals = all.filter(function(dg) { return !!dg.reverses; }); + var html = '
Procedural Sedation — ' + wt + ' kg patient
'; // Sedation agents html += '

Sedation Agents

'; html += '
'; - - // Small helper: display dose ranges with per-kg footer consistently. - var perKg = function(lo, hi, unit) { - unit = unit || 'mg'; - return ' (' + lo + (hi != null ? '-' + hi : '') + ' ' + unit + '/kg)'; - }; - html += ''; - html += ''; - html += ''; - html += ''; - html += ''; - html += ''; - html += ''; - html += ''; - html += ''; - + html += agents.map(function(dg) { return sedRow(wt, dg, false); }).join(''); html += '
DrugDoseRouteOnsetDurationNotes
Ketamine' + d(wt, 1.5, 100) + '-' + d(wt, 2, 150) + ' mg' + perKg(1.5, 2) + 'IV1 min15-30 minDissociative. Preserves airway reflexes.
' + d(wt, 4, 300) + '-' + d(wt, 5, 400) + ' mg' + perKg(4, 5) + 'IM3-5 min30-60 minGive atropine 0.01 mg/kg to reduce secretions.
Midazolam' + d(wt, 0.05, 2) + '-' + d(wt, 0.1, 5) + ' mg' + perKg(0.05, 0.1) + 'IV2-3 min30-60 minAnxiolysis. Titrate q3-5 min.
' + d(wt, 0.5, 20) + ' mg' + perKg(0.5, null) + 'IN/PO10-15 min30-60 minIN (max 10 mg / naris) or PO (max 20 mg).
Propofol' + d(wt, 1, 40) + ' mg' + perKg(1, null) + 'IV30 sec5-10 minShort procedures. Causes apnea — manage airway.
Fentanyl' + d(wt, 1, 100) + ' mcg' + perKg(1, null, 'mcg') + 'IV2-3 min30-60 minAnalgesic. Often combined with midazolam.
' + d(wt, 2, 100) + ' mcg' + perKg(2, null, 'mcg') + 'IN5-10 min30-60 minIntranasal.
Nitrous oxide50:50 or 70:30 mixInhaled2-5 min5 min offSelf-administered via demand valve. Anxiolysis + mild analgesia.
Dexmedetomidine' + d(wt, 2, 100) + ' mcg' + perKg(2, 3, 'mcg') + 'IN15-30 min60-90 minIntranasal. No respiratory depression. Good for imaging.
'; // Reversal agents html += '

Reversal Agents

'; html += '
'; - html += ''; - html += ''; + html += reversals.map(function(dg) { return sedRow(wt, dg, true); }).join(''); html += '
DrugDoseRouteReversesNotes
Naloxone' + d(wt, 0.1, 2) + ' mg' + perKg(0.1, null) + 'IV/IM/INOpioids (fentanyl, morphine)Max 2 mg. Repeat q2-3 min. Duration shorter than opioids — monitor for re-sedation.
Flumazenil' + d(wt, 0.01, 0.2) + ' mg' + perKg(0.01, null) + 'IVBenzodiazepines (midazolam)Max 0.2 mg single dose. Repeat q1 min to max 1 mg total. Risk of seizures — use cautiously.
'; html += '
Pre-sedation checklist: NPO status (2h clear liquids, 6h solids), consent, monitoring equipment (pulse ox, capnography, BP), resuscitation equipment at bedside, suction ready, IV access. Minimum monitoring: continuous SpO2, HR, capnography. Provider capable of managing airway must be present.
'; @@ -3306,30 +3421,78 @@ window._EM = { if (e.target.id === 'btn-agit-calc' || e.target.closest('#btn-agit-calc')) calcAgit(); }); + // 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(d) { + var fb = AGIT_FALLBACK.filter(function(x) { return x.name === d.name; })[0] || {}; + return Object.assign({}, fb, d); + }); + } + 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, d) { + var S = window._EM; + var dose; + if (d.dose_display_if_under_30kg && d.dose_display_if_30kg_or_more) { + dose = wt < 30 ? d.dose_display_if_under_30kg : d.dose_display_if_30kg_or_more; + } else if (d.dose_display_if_under_40kg && d.dose_display_if_40kg_or_more) { + dose = wt < 40 ? d.dose_display_if_under_40kg + ' = ' + S.d(wt, d.dose_mg_per_kg) + ' mg' : d.dose_display_if_40kg_or_more; + } else if (d.dose_display_prefix) { + dose = d.dose_display_prefix + ' = ' + S.d(wt, d.dose_mg_per_kg) + ' mg'; + } else if (d.dose_mg_per_kg != null) { + dose = S.dStr(wt, d.dose_mg_per_kg, d.max_mg, ' ' + d.unit); + } else { + dose = d.dose_display || ''; + } + return S.drugRow(d.display || d.name, dose, d.route, d.notes); + } + + // Haloperidol has a weight-specific quirk: under 40 kg the display line + // needs the computed mg, but over 40 kg it's just "2.5-5 mg". Handled + // uniformly by dose_display_if_under_40kg above. + 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 S = window._EM; + var drugs = agitDrugs(); + var step2 = drugs.filter(function(d) { return d.step === 2; }); + var step3 = drugs.filter(function(d) { return d.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( - S.drugRow('Lorazepam', S.dStr(wt, 0.05, 2, ' mg'), 'PO', '0.05 mg/kg. May repeat in 30 min.') + - S.drugRow('Midazolam', S.dStr(wt, 0.5, 10, ' mg'), 'PO', '0.5 mg/kg (max 10 mg)') + - S.drugRow('Midazolam', S.dStr(wt, 0.3, 10, ' mg'), 'IN (5 mg/mL)', '0.3 mg/kg split between nares (max 10 mg)') + - S.drugRow('Olanzapine (ODT)', wt < 30 ? '2.5-5 mg' : '5-10 mg', 'PO / ODT', 'Age ≥6 yr. Avoid IM + benzo combo (risk of resp depression).') + - S.drugRow('Diphenhydramine', S.dStr(wt, 1, 50, ' mg'), 'PO', '1 mg/kg. Adjunct only; sedating.') - ); + html += S.drugTable(step2.map(function(d) { return agitRowFor(wt, d); }).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( - S.drugRow('Midazolam', S.dStr(wt, 0.1, 5, ' mg'), 'IV / IM', '0.05-0.1 mg/kg IV, 0.1-0.15 mg/kg IM (max 10 mg)') + - S.drugRow('Lorazepam', S.dStr(wt, 0.1, 4, ' mg'), 'IV / IM', '0.05-0.1 mg/kg (max 4 mg). May cause resp depression.') + - S.drugRow('Haloperidol', wt < 40 ? '0.025-0.075 mg/kg = ' + S.d(wt, 0.05) + ' mg' : '2.5-5 mg', 'IM / IV', 'Avoid <3 yr. Risk: QT, EPS, NMS. ECG if repeated.') + - S.drugRow('Olanzapine', wt < 40 ? '2.5-5 mg' : '5-10 mg', 'IM', 'Avoid benzo co-administration (resp depression, hypotension)') + - S.drugRow('Ketamine', S.dStr(wt, 4, 500, ' mg'), 'IM', '4-5 mg/kg IM (rescue for severe excited delirium). Monitor airway.') + - S.drugRow('Droperidol', '0.03-0.07 mg/kg = ' + S.d(wt, 0.05) + ' mg', 'IM / IV', 'Effective but QT concern — get ECG.') - ); + html += S.drugTable(step3.map(function(d) { return agitRowFor(wt, d); }).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; @@ -3344,23 +3507,50 @@ window._EM = { if (e.target.id === 'btn-emet-calc' || e.target.closest('#btn-emet-calc')) calcEmet(); }); + // 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) { + var S = window._EM; + return drugs.map(function(d) { + var dose; + if (d.name === 'Ondansetron' && d.weight_band_dose) { + var bd = d.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, d.dose_mg_per_kg, d.max_mg, ' ' + d.unit) + ' (' + d.dose_mg_per_kg + ' ' + d.unit + '/kg)'; + } else if (d.dose_display) { + dose = d.dose_display; + } else { + dose = S.dStr(wt, d.dose_mg_per_kg, d.max_mg, ' ' + d.unit); + } + return S.drugRow(d.name, dose, d.route, d.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 S = window._EM; - // Ondansetron weight bands - var ondWt = wt < 15 ? '2 mg' : wt < 30 ? '4 mg' : '8 mg'; var html = '
Antiemetic doses — ' + wt + ' kg
'; - html += S.drugTable( - S.drugRow('Ondansetron', ondWt + ' (weight band) OR ' + S.dStr(wt, 0.15, 8, ' mg') + ' (0.15 mg/kg)', 'PO / ODT / IV', 'First-line. Max single 8 mg. Repeat q8h. QT prolongation — avoid with other QT drugs. <6 mo: limited data.') + - S.drugRow('Metoclopramide', S.dStr(wt, 0.15, 10, ' mg'), 'IV / IM / PO', '0.1-0.15 mg/kg (max 10 mg). Give with diphenhydramine to prevent EPS / dystonia.') + - S.drugRow('Dimenhydrinate', S.dStr(wt, 1.25, 50, ' mg'), 'PO / IV / IM / PR', '1.25 mg/kg (max 50 mg) q6h. ≥2 yr.') + - S.drugRow('Diphenhydramine', S.dStr(wt, 1, 50, ' mg'), 'PO / IV / IM', '1 mg/kg (max 50 mg) q6h. Adjunct, sedating.') + - S.drugRow('Promethazine', S.dStr(wt, 0.25, 25, ' mg'), 'PO / IV / IM', '0.25-1 mg/kg. CONTRAINDICATED <2 yr (resp depression). Tissue injury if IV extrav.') + - S.drugRow('Dexamethasone', S.dStr(wt, 0.15, 10, ' mg'), 'IV / PO', 'Adjunct, esp. chemo-induced. 0.15 mg/kg (max 10 mg).') + - S.drugRow('Scopolamine patch', '1.5 mg patch', 'Transdermal', '≥12 yr. Motion sickness. Apply 4h before exposure.') - ); + 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; diff --git a/public/js/drugs-loader.js b/public/js/drugs-loader.js new file mode 100644 index 0000000..f517d0a --- /dev/null +++ b/public/js/drugs-loader.js @@ -0,0 +1,31 @@ +// ============================================================ +// drugs-loader.js +// Fetches /data/drugs.json at page load and exposes: +// window._DRUGS — the parsed JSON (after fetch resolves) +// window._DRUGS_READY — a Promise that resolves to the JSON +// +// Calculator functions in calculators.js look up drugs via a small +// helper (pickDrug) and fall back to hardcoded inline data if the +// JSON isn't loaded yet. This means the app keeps working even if +// the fetch fails — the JSON is an authoritative data source but +// not a hard dependency at runtime. +// ============================================================ +(function() { + // Default to empty shape so lookups don't throw before fetch resolves. + window._DRUGS = window._DRUGS || { version: null, sections: {} }; + + window._DRUGS_READY = fetch('/data/drugs.json', { credentials: 'same-origin' }) + .then(function(r) { + if (!r.ok) throw new Error('drugs.json HTTP ' + r.status); + return r.json(); + }) + .then(function(j) { + window._DRUGS = j; + return j; + }) + .catch(function(err) { + // Non-fatal — calc functions have inline fallbacks. + console.warn('[drugs-loader] could not load /data/drugs.json:', err); + return window._DRUGS; + }); +})(); diff --git a/test/calc-math.test.js b/test/calc-math.test.js index 861b3f3..0810e01 100644 --- a/test/calc-math.test.js +++ b/test/calc-math.test.js @@ -165,3 +165,20 @@ test('Lund-Browder: 50% of both thighs young child = 8%', () => { test('Lund-Browder: invalid bracket returns null', () => { assert.equal(M.lundBrowderTBSA('bogus', { head: 50 }), null); }); + +// ── drugs.json schema ─────────────────────────────────────────── +// Asserts the JSON is loadable and that each of the 5 extracted +// bedside sections exists with a non-empty drugs array. +test('drugs.json: schema loads with all 5 extracted sections + non-empty drugs', () => { + const fs = require('node:fs'); + const path = require('node:path'); + const raw = fs.readFileSync(path.join(__dirname, '..', 'public', 'data', 'drugs.json'), 'utf8'); + const j = JSON.parse(raw); + assert.equal(j.version, '1.0'); + assert.ok(j.sections, 'top-level sections object present'); + ['anaphylaxis', 'sedation', 'agitation', 'antiemetics', 'seizure'].forEach((key) => { + const sec = j.sections[key]; + assert.ok(sec, `section ${key} present`); + assert.ok(Array.isArray(sec.drugs) && sec.drugs.length > 0, `${key}.drugs non-empty`); + }); +});