From 6a081bb53bf9b729c28b641a55d5a2e114e61277 Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 8 May 2026 03:18:37 +0200 Subject: [PATCH] extract calculator reference data --- public/data/calculators/equipment.json | 15 + public/data/calculators/fenton-lms.json | 52 ++++ public/data/calculators/resus-meds.json | 24 ++ public/js/calculators.js | 396 ++---------------------- public/js/calculators/equipment.js | 38 +++ public/js/calculators/neonatal.js | 70 +++++ public/js/calculators/resus.js | 101 ++++++ test/calculators-data.test.js | 81 +++++ 8 files changed, 405 insertions(+), 372 deletions(-) create mode 100644 public/data/calculators/equipment.json create mode 100644 public/data/calculators/fenton-lms.json create mode 100644 public/data/calculators/resus-meds.json create mode 100644 public/js/calculators/equipment.js create mode 100644 public/js/calculators/neonatal.js create mode 100644 public/js/calculators/resus.js diff --git a/public/data/calculators/equipment.json b/public/data/calculators/equipment.json new file mode 100644 index 0000000..10c9340 --- /dev/null +++ b/public/data/calculators/equipment.json @@ -0,0 +1,15 @@ +{ + "version": "1.0", + "source": "Harriet Lane Handbook", + "ageGroups": { + "premie": { "label": "Premie (1-3 kg)", "bvm": "Infant", "nasal": "12 Fr", "oral": "Infant", "blade": "Miller 0", "ett": "2.5-3.0", "lma": "1", "glidescope": "1", "iv": "22-24 ga", "cvl": "3 Fr", "ngt": "5 Fr", "chest": "10-12 Fr", "foley": "6 Fr" }, + "newborn": { "label": "Newborn (2-4 kg)", "bvm": "Infant", "nasal": "14-16 Fr", "oral": "Small 50 mm", "blade": "Miller 0", "ett": "3.0-3.5", "lma": "1", "glidescope": "1", "iv": "22-24 ga", "cvl": "3-4 Fr", "ngt": "5-8 Fr", "chest": "10-12 Fr", "foley": "6 Fr" }, + "6mo": { "label": "6 months (6-8 kg)", "bvm": "Infant", "nasal": "14-16 Fr", "oral": "Small 60 mm", "blade": "Miller 1", "ett": "3.5", "lma": "1.5", "glidescope": "2", "iv": "20-24 ga", "cvl": "4 Fr", "ngt": "8 Fr", "chest": "12-18 Fr", "foley": "8 Fr" }, + "1yr": { "label": "1 year (10 kg)", "bvm": "Small child", "nasal": "14-18 Fr", "oral": "Small 60 mm", "blade": "Miller 1 / MAC 2", "ett": "4.0", "lma": "2", "glidescope": "2", "iv": "20-24 ga", "cvl": "4-5 Fr", "ngt": "10 Fr", "chest": "16-20 Fr", "foley": "8 Fr" }, + "2-3yr": { "label": "2-3 years (12-16 kg)", "bvm": "Small child", "nasal": "14-18 Fr", "oral": "Small 70 mm", "blade": "Miller 1 / MAC 2", "ett": "4.0-4.5", "lma": "2", "glidescope": "2", "iv": "18-22 ga", "cvl": "4-5 Fr", "ngt": "10-12 Fr", "chest": "16-24 Fr", "foley": "8 Fr" }, + "4-6yr": { "label": "4-6 years (20-25 kg)", "bvm": "Child", "nasal": "16-20 Fr", "oral": "Small 70-80 mm", "blade": "Miller 2 / MAC 2", "ett": "4.5-5.0", "lma": "2.5", "glidescope": "3", "iv": "18-22 ga", "cvl": "5 Fr", "ngt": "12-14 Fr", "chest": "20-28 Fr", "foley": "8 Fr" }, + "7-10yr": { "label": "7-10 years (25-35 kg)", "bvm": "Child / Small adult", "nasal": "18-22 Fr", "oral": "Medium 80-90 mm", "blade": "Miller 2 / MAC 2", "ett": "5.5-6.0", "lma": "2.5-3", "glidescope": "3", "iv": "18-22 ga", "cvl": "5 Fr", "ngt": "12-14 Fr", "chest": "20-32 Fr", "foley": "8 Fr" }, + "11-15yr": { "label": "11-15 years (40-50 kg)", "bvm": "Adult", "nasal": "22-36 Fr", "oral": "Medium 90 mm", "blade": "Miller 2 / MAC 3", "ett": "6.0-6.5", "lma": "3", "glidescope": "3 or 4", "iv": "18-20 ga", "cvl": "7 Fr", "ngt": "14-18 Fr", "chest": "28-38 Fr", "foley": "10 Fr" }, + "16yr": { "label": "16+ years (>50 kg)", "bvm": "Adult", "nasal": "22-36 Fr", "oral": "Medium 90 mm", "blade": "Miller 2 / MAC 3", "ett": "7.0-8.0", "lma": "4", "glidescope": "3 or 4", "iv": "18-20 ga", "cvl": "7 Fr", "ngt": "14-18 Fr", "chest": "28-42 Fr", "foley": "12 Fr" } + } +} diff --git a/public/data/calculators/fenton-lms.json b/public/data/calculators/fenton-lms.json new file mode 100644 index 0000000..894c332 --- /dev/null +++ b/public/data/calculators/fenton-lms.json @@ -0,0 +1,52 @@ +{ + "version": "1.0", + "source": "Fenton TR, Kim JH. BMC Pediatrics 2013;13:59", + "weightLms": { + "male": { + "22": { "L": 0.21, "M": 496, "S": 0.17 }, + "23": { "L": 0.21, "M": 575, "S": 0.17 }, + "24": { "L": 0.21, "M": 660, "S": 0.17 }, + "25": { "L": 0.21, "M": 762, "S": 0.16 }, + "26": { "L": 0.21, "M": 870, "S": 0.16 }, + "27": { "L": 0.20, "M": 993, "S": 0.15 }, + "28": { "L": 0.20, "M": 1124, "S": 0.15 }, + "29": { "L": 0.19, "M": 1272, "S": 0.14 }, + "30": { "L": 0.18, "M": 1430, "S": 0.14 }, + "31": { "L": 0.17, "M": 1607, "S": 0.14 }, + "32": { "L": 0.15, "M": 1795, "S": 0.14 }, + "33": { "L": 0.13, "M": 2008, "S": 0.13 }, + "34": { "L": 0.12, "M": 2230, "S": 0.13 }, + "35": { "L": 0.10, "M": 2467, "S": 0.13 }, + "36": { "L": 0.08, "M": 2710, "S": 0.13 }, + "37": { "L": 0.06, "M": 2948, "S": 0.12 }, + "38": { "L": 0.04, "M": 3195, "S": 0.12 }, + "39": { "L": 0.02, "M": 3380, "S": 0.12 }, + "40": { "L": 0.01, "M": 3530, "S": 0.12 }, + "41": { "L": 0.00, "M": 3660, "S": 0.12 }, + "42": { "L": -0.02, "M": 3820, "S": 0.12 } + }, + "female": { + "22": { "L": 0.23, "M": 474, "S": 0.17 }, + "23": { "L": 0.22, "M": 538, "S": 0.17 }, + "24": { "L": 0.22, "M": 610, "S": 0.17 }, + "25": { "L": 0.22, "M": 705, "S": 0.16 }, + "26": { "L": 0.22, "M": 810, "S": 0.16 }, + "27": { "L": 0.21, "M": 920, "S": 0.15 }, + "28": { "L": 0.21, "M": 1040, "S": 0.15 }, + "29": { "L": 0.20, "M": 1178, "S": 0.14 }, + "30": { "L": 0.19, "M": 1330, "S": 0.14 }, + "31": { "L": 0.18, "M": 1500, "S": 0.14 }, + "32": { "L": 0.16, "M": 1680, "S": 0.14 }, + "33": { "L": 0.14, "M": 1880, "S": 0.13 }, + "34": { "L": 0.12, "M": 2090, "S": 0.13 }, + "35": { "L": 0.10, "M": 2310, "S": 0.13 }, + "36": { "L": 0.08, "M": 2540, "S": 0.13 }, + "37": { "L": 0.06, "M": 2766, "S": 0.12 }, + "38": { "L": 0.04, "M": 3000, "S": 0.12 }, + "39": { "L": 0.02, "M": 3180, "S": 0.12 }, + "40": { "L": 0.01, "M": 3340, "S": 0.12 }, + "41": { "L": 0.00, "M": 3480, "S": 0.12 }, + "42": { "L": -0.02, "M": 3630, "S": 0.12 } + } + } +} diff --git a/public/data/calculators/resus-meds.json b/public/data/calculators/resus-meds.json new file mode 100644 index 0000000..aceaf7b --- /dev/null +++ b/public/data/calculators/resus-meds.json @@ -0,0 +1,24 @@ +{ + "version": "1.0", + "source": "Harriet Lane Handbook / AHA PALS 2020", + "categories": { + "cardiac": { "label": "Cardiac", "color": "#ef4444" }, + "metabolic": { "label": "Metabolic", "color": "#3b82f6" }, + "reversal": { "label": "Reversal", "color": "#10b981" } + }, + "medications": [ + { "name": "Adenosine", "indication": "SVT", "category": "cardiac", "route": "IV/IO rapid bolus", "formula": "adenosine" }, + { "name": "Amiodarone", "indication": "VT / VF", "category": "cardiac", "route": "IV/IO", "formula": "amiodarone" }, + { "name": "Atropine", "indication": "Bradycardia", "category": "cardiac", "route": "IV/IO/IM", "formula": "atropine" }, + { "name": "Calcium Chloride 10%", "indication": "Hypocalcemia / Hyperkalemia", "category": "metabolic", "route": "IV/IO", "formula": "calciumChloride" }, + { "name": "Calcium Gluconate 10%", "indication": "Hypocalcemia / Hyperkalemia", "category": "metabolic", "route": "IV/IO", "formula": "calciumGluconate" }, + { "name": "Dextrose", "indication": "Hypoglycemia", "category": "metabolic", "route": "IV", "formula": "dextrose" }, + { "name": "Epinephrine", "indication": "Pulseless arrest / Anaphylaxis", "category": "cardiac", "route": "IV/IO/IM/ETT", "formula": "epinephrine" }, + { "name": "Hydrocortisone", "indication": "Adrenal crisis", "category": "metabolic", "route": "IV/IM/IO", "formula": "hydrocortisone" }, + { "name": "Insulin (Regular)", "indication": "Hyperkalemia", "category": "metabolic", "route": "IV", "formula": "insulin" }, + { "name": "Lidocaine", "indication": "Antiarrhythmic", "category": "cardiac", "route": "IV/IO", "formula": "lidocaine" }, + { "name": "Magnesium Sulfate", "indication": "Torsades de Pointes", "category": "cardiac", "route": "IV/IO", "formula": "magnesiumSulfate" }, + { "name": "Naloxone", "indication": "Opioid overdose", "category": "reversal", "route": "IV/IO/IM/IN/ETT", "formula": "naloxone" }, + { "name": "Sodium Bicarbonate", "indication": "Metabolic acidosis", "category": "metabolic", "route": "IV/IO", "formula": "sodiumBicarbonate" } + ] +} diff --git a/public/js/calculators.js b/public/js/calculators.js index 93d140c..eabce8b 100644 --- a/public/js/calculators.js +++ b/public/js/calculators.js @@ -1,5 +1,8 @@ import { parseAgeMonths, formatParsedAge } from './calculators/age.js'; +import { loadEquipmentData, renderEquipmentResult } from './calculators/equipment.js'; import { initImageLightbox } from './calculators/lightbox.js'; +import { calcZ as calcNeonatalZ, classifyBirthWeight, classifyGA, classifyWeight, interpolateLMS, loadFentonLmsData, zToPercentile } from './calculators/neonatal.js'; +import { loadResusMedsData, renderResusMeds } from './calculators/resus.js'; import { loadVitalsData, renderVitalsResult } from './calculators/vitals.js'; // ============================================================ @@ -1689,187 +1692,10 @@ initImageLightbox(); // RESUSCITATION MEDICATIONS — Harriet Lane Handbook / AHA PALS 2020 // ═══════════════════════════════════════════════════════════ - var RESUS_MEDS = [ - { - name: 'Adenosine', indication: 'SVT', category: 'cardiac', - route: 'IV/IO rapid bolus', - calc: function(w) { - var d1 = +(w * 0.1).toFixed(2); - var d2 = +(w * 0.2).toFixed(2); - var d3 = +(w * 0.3).toFixed(2); - return { - dose: d1 + ' mg (0.1 mg/kg)', - extra: 'May repeat: ' + Math.min(d2, 12) + ' mg (0.2 mg/kg), then ' + Math.min(d3, 12) + ' mg (0.3 mg/kg)', - max: 'Max first dose 6 mg, max subsequent 12 mg' - }; - } - }, - { - name: 'Amiodarone', indication: 'VT / VF', category: 'cardiac', - route: 'IV/IO', - calc: function(w) { - var d = +(w * 5).toFixed(1); - return { - dose: Math.min(d, 300) + ' mg (5 mg/kg)', - extra: 'No pulse: push undiluted. Pulse: over 20-60 min. Subsequent max 150 mg.', - max: 'Max first 300 mg, max total 15 mg/kg/24hr or 2200 mg' - }; - } - }, - { - name: 'Atropine', indication: 'Bradycardia', category: 'cardiac', - route: 'IV/IO/IM', - calc: function(w) { - var d = +(w * 0.02).toFixed(3); - var ett = (w * 0.04).toFixed(3) + '-' + (w * 0.06).toFixed(3); - return { - dose: Math.min(d, 0.5) + ' mg (0.02 mg/kg)', - extra: 'ETT dose: ' + ett + ' mg (0.04-0.06 mg/kg)', - max: 'Max single 0.5 mg, max total 1 mg' - }; - } - }, - { - name: 'Calcium Chloride 10%', indication: 'Hypocalcemia / Hyperkalemia', category: 'metabolic', - route: 'IV/IO', - calc: function(w) { - var d = +(w * 20).toFixed(0); - return { - dose: Math.min(d, 1000) + ' mg (20 mg/kg)', - extra: 'Give slowly. Central line preferred.', - max: 'Max 1 g (1000 mg)' - }; - } - }, - { - name: 'Calcium Gluconate 10%', indication: 'Hypocalcemia / Hyperkalemia', category: 'metabolic', - route: 'IV/IO', - calc: function(w) { - var d = +(w * 60).toFixed(0); - return { - dose: Math.min(d, 3000) + ' mg (60 mg/kg)', - extra: 'Give slowly over 10-20 min with cardiac monitoring.', - max: 'Max 3 g (3000 mg)' - }; - } - }, - { - name: 'Dextrose', indication: 'Hypoglycemia', category: 'metabolic', - route: 'IV', - calc: function(w) { - var grams = +(w * 0.5).toFixed(1) + '-' + +(w * 1).toFixed(1); - var detail = ''; - if (w < 5) { - var ml = (w * 5).toFixed(1) + '-' + (w * 10).toFixed(1); - detail = 'D10W: ' + ml + ' mL (5-10 mL/kg)'; - } else if (w < 45) { - var ml = (w * 2).toFixed(1) + '-' + (w * 4).toFixed(1); - detail = 'D25W: ' + ml + ' mL (2-4 mL/kg)'; - } else { - var ml = (w * 1).toFixed(1) + '-' + (w * 2).toFixed(1); - detail = 'D50W: ' + ml + ' mL (1-2 mL/kg)'; - } - return { - dose: grams + ' g (0.5-1 g/kg)', - extra: detail, - max: 'Max 25 g' - }; - } - }, - { - name: 'Epinephrine', indication: 'Pulseless arrest / Anaphylaxis', category: 'cardiac', - route: 'IV/IO/IM/ETT', - calc: function(w) { - var iv = +(w * 0.01).toFixed(3); - var ivVol = +(w * 0.1).toFixed(2); - var ett = +(w * 0.1).toFixed(2); - var im = +(w * 0.01).toFixed(3); - return { - dose: Math.min(iv, 1) + ' mg IV/IO (0.01 mg/kg of 0.1 mg/mL = ' + Math.min(ivVol, 10) + ' mL) q3-5 min', - extra: 'ETT: ' + Math.min(ett, 2.5) + ' mg (0.1 mg/kg of 1 mg/mL). Anaphylaxis IM: ' + Math.min(im, 0.5) + ' mg (0.01 mg/kg)', - max: 'Max IV 1 mg, max ETT 2.5 mg, max IM 0.5 mg' - }; - } - }, - { - name: 'Hydrocortisone', indication: 'Adrenal crisis', category: 'metabolic', - route: 'IV/IM/IO', - calc: function(w) { - var d = +(w * 2).toFixed(1); - return { - dose: Math.min(d, 100) + ' mg (2 mg/kg)', - extra: 'Stress dosing for adrenal insufficiency.', - max: 'Max 100 mg' - }; - } - }, - { - name: 'Insulin (Regular)', indication: 'Hyperkalemia', category: 'metabolic', - route: 'IV', - calc: function(w) { - var d = +(w * 0.1).toFixed(2); - var dex = +(w * 0.5).toFixed(1); - return { - dose: Math.min(d, 5) + ' units (0.1 units/kg)', - extra: 'Give with ' + dex + ' g/kg dextrose (0.5 g/kg). Monitor glucose closely.', - max: 'Max 5 units' - }; - } - }, - { - name: 'Lidocaine', indication: 'Antiarrhythmic', category: 'cardiac', - route: 'IV/IO', - calc: function(w) { - var d = +(w * 1).toFixed(1); - var ett = (w * 2).toFixed(1) + '-' + (w * 3).toFixed(1); - return { - dose: Math.min(d, 100) + ' mg (1 mg/kg)', - extra: 'ETT: ' + ett + ' mg (2-3 mg/kg). May repeat q5 min.', - max: 'Max 100 mg/dose, max total 3 mg/kg' - }; - } - }, - { - name: 'Magnesium Sulfate', indication: 'Torsades de Pointes', category: 'cardiac', - route: 'IV/IO', - calc: function(w) { - var d = +(w * 50).toFixed(0); - return { - dose: Math.min(d, 2000) + ' mg (50 mg/kg)', - extra: 'Give over 10-20 min (faster if pulseless).', - max: 'Max 2 g (2000 mg)' - }; - } - }, - { - name: 'Naloxone', indication: 'Opioid overdose', category: 'reversal', - route: 'IV/IO/IM/IN/ETT', - calc: function(w) { - var partial = +(w * 0.001).toFixed(4) + '-' + +(w * 0.005).toFixed(4); - var full = +(w * 0.1).toFixed(3); - return { - dose: 'Partial: ' + partial + ' mg (0.001-0.005 mg/kg)', - extra: 'Full reversal: ' + Math.min(full, 2) + ' mg (0.1 mg/kg)', - max: 'Max partial first dose 0.1 mg, max full 2 mg' - }; - } - }, - { - name: 'Sodium Bicarbonate', indication: 'Metabolic acidosis', category: 'metabolic', - route: 'IV/IO', - calc: function(w) { - var d = +(w * 1).toFixed(1); - return { - dose: Math.min(d, 50) + ' mEq (1 mEq/kg)', - extra: w < 10 ? 'Dilute to 0.5 mEq/mL (use 4.2% solution) for neonates/small infants.' : 'Use 8.4% solution (1 mEq/mL).', - max: 'Max 50 mEq' - }; - } - } - ]; - - var categoryColors = { cardiac: '#ef4444', metabolic: '#3b82f6', reversal: '#10b981' }; - var categoryLabels = { cardiac: 'Cardiac', metabolic: 'Metabolic', reversal: 'Reversal' }; + var RESUS_DATA = null; + loadResusMedsData() + .then(function(data) { RESUS_DATA = data; }) + .catch(function(err) { console.warn('[calculators] Resus meds data unavailable:', err && err.message); }); // Use event delegation for resus panel var resusPanel = document.getElementById('calc-resus'); @@ -1890,37 +1716,8 @@ initImageLightbox(); var w = parseFloat(weightEl.value); if (!w || w <= 0) { showToast('Please enter a valid weight.', 'error'); return; } - var html = '
Doses for ' + w + ' kg patient
'; - html += '
'; - Object.keys(categoryColors).forEach(function(cat) { - html += '' + categoryLabels[cat] + ''; - }); - html += '
'; - html += '
'; - - RESUS_MEDS.forEach(function(med) { - var r = med.calc(w); - var color = categoryColors[med.category] || '#6b7280'; - html += '
' + - '
' + - '
' + med.name + '
' + - '
' + med.indication + '
' + - '
' + - '
' + - '
Dose: ' + r.dose + '
' + - '
' + r.extra + '
' + - '
Max: ' + r.max + '
' + - '
Route: ' + med.route + '
' + - '
' + - '
'; - }); - - html += '
'; - html += '
' + - 'Disclaimer: Always verify doses against institutional protocols and current guidelines. This tool is for educational reference only.' + - '
'; - - resultDiv.innerHTML = html; + if (!RESUS_DATA) { showToast('Resus medication data is still loading.', 'info'); return; } + resultDiv.innerHTML = renderResusMeds(RESUS_DATA, w); resultDiv.classList.remove('hidden'); }); } @@ -1989,62 +1786,10 @@ initImageLightbox(); // EQUIPMENT SIZING — Harriet Lane Handbook // ═══════════════════════════════════════════════════════════ - var EQUIP_DATA = { - premie: { - label: 'Premie (1-3 kg)', - bvm: 'Infant', nasal: '12 Fr', oral: 'Infant', blade: 'Miller 0', - ett: '2.5-3.0', lma: '1', glidescope: '1', - iv: '22-24 ga', cvl: '3 Fr', ngt: '5 Fr', chest: '10-12 Fr', foley: '6 Fr' - }, - newborn: { - label: 'Newborn (2-4 kg)', - bvm: 'Infant', nasal: '14-16 Fr', oral: 'Small 50 mm', blade: 'Miller 0', - ett: '3.0-3.5', lma: '1', glidescope: '1', - iv: '22-24 ga', cvl: '3-4 Fr', ngt: '5-8 Fr', chest: '10-12 Fr', foley: '6 Fr' - }, - '6mo': { - label: '6 months (6-8 kg)', - bvm: 'Infant', nasal: '14-16 Fr', oral: 'Small 60 mm', blade: 'Miller 1', - ett: '3.5', lma: '1.5', glidescope: '2', - iv: '20-24 ga', cvl: '4 Fr', ngt: '8 Fr', chest: '12-18 Fr', foley: '8 Fr' - }, - '1yr': { - label: '1 year (10 kg)', - bvm: 'Small child', nasal: '14-18 Fr', oral: 'Small 60 mm', blade: 'Miller 1 / MAC 2', - ett: '4.0', lma: '2', glidescope: '2', - iv: '20-24 ga', cvl: '4-5 Fr', ngt: '10 Fr', chest: '16-20 Fr', foley: '8 Fr' - }, - '2-3yr': { - label: '2-3 years (12-16 kg)', - bvm: 'Small child', nasal: '14-18 Fr', oral: 'Small 70 mm', blade: 'Miller 1 / MAC 2', - ett: '4.0-4.5', lma: '2', glidescope: '2', - iv: '18-22 ga', cvl: '4-5 Fr', ngt: '10-12 Fr', chest: '16-24 Fr', foley: '8 Fr' - }, - '4-6yr': { - label: '4-6 years (20-25 kg)', - bvm: 'Child', nasal: '16-20 Fr', oral: 'Small 70-80 mm', blade: 'Miller 2 / MAC 2', - ett: '4.5-5.0', lma: '2.5', glidescope: '3', - iv: '18-22 ga', cvl: '5 Fr', ngt: '12-14 Fr', chest: '20-28 Fr', foley: '8 Fr' - }, - '7-10yr': { - label: '7-10 years (25-35 kg)', - bvm: 'Child / Small adult', nasal: '18-22 Fr', oral: 'Medium 80-90 mm', blade: 'Miller 2 / MAC 2', - ett: '5.5-6.0', lma: '2.5-3', glidescope: '3', - iv: '18-22 ga', cvl: '5 Fr', ngt: '12-14 Fr', chest: '20-32 Fr', foley: '8 Fr' - }, - '11-15yr': { - label: '11-15 years (40-50 kg)', - bvm: 'Adult', nasal: '22-36 Fr', oral: 'Medium 90 mm', blade: 'Miller 2 / MAC 3', - ett: '6.0-6.5', lma: '3', glidescope: '3 or 4', - iv: '18-20 ga', cvl: '7 Fr', ngt: '14-18 Fr', chest: '28-38 Fr', foley: '10 Fr' - }, - '16yr': { - label: '16+ years (>50 kg)', - bvm: 'Adult', nasal: '22-36 Fr', oral: 'Medium 90 mm', blade: 'Miller 2 / MAC 3', - ett: '7.0-8.0', lma: '4', glidescope: '3 or 4', - iv: '18-20 ga', cvl: '7 Fr', ngt: '14-18 Fr', chest: '28-42 Fr', foley: '12 Fr' - } - }; + var EQUIP_DATA = {}; + loadEquipmentData() + .then(function(data) { EQUIP_DATA = data; }) + .catch(function(err) { console.warn('[calculators] Equipment data unavailable:', err && err.message); }); var equipPanel = document.getElementById('calc-equipment'); if (equipPanel) { @@ -2056,32 +1801,7 @@ initImageLightbox(); var eq = EQUIP_DATA[key]; resultDiv.classList.remove('hidden'); - resultDiv.innerHTML = - '
' + - '
' + eq.label + '
' + - '
' + - '
' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '
Bag-Valve Mask' + eq.bvm + '
Oral Airway' + eq.oral + '
Nasal Airway' + eq.nasal + '
Laryngoscope Blade' + eq.blade + '
ETT Size' + eq.ett + '
LMA Size' + eq.lma + '
Glidescope' + eq.glidescope + '
IV Catheter' + eq.iv + '
Central Line' + eq.cvl + '
NGT / OGT' + eq.ngt + '
Chest Tube' + eq.chest + '
Foley Catheter' + eq.foley + '
' + - '
' + - '
' + - 'ETT Formulas: Uncuffed = (age/4)+4, Cuffed = (age/4)+3, Depth = ETT size x 3. ' + - 'Source: Harriet Lane Handbook.' + - '
'; + resultDiv.innerHTML = renderEquipmentResult(eq); }); } } @@ -2091,87 +1811,15 @@ initImageLightbox(); // NEONATAL ASSESSMENT // ============================================================ (function() { + var fentonLMS = { male: {}, female: {} }; + loadFentonLmsData() + .then(function(data) { fentonLMS = data; }) + .catch(function(err) { console.warn('[calculators] Fenton LMS data unavailable:', err && err.message); }); + document.addEventListener('click', function(e) { if (e.target.id === 'btn-neo-assess' || e.target.closest('#btn-neo-assess')) neoAssess(); }); - // Fenton 2013 LMS data (weight in grams, GA in weeks) - var fentonLMS = { - male: { - 22:{L:0.21,M:496,S:0.17},23:{L:0.21,M:575,S:0.17},24:{L:0.21,M:660,S:0.17},25:{L:0.21,M:762,S:0.16}, - 26:{L:0.21,M:870,S:0.16},27:{L:0.20,M:993,S:0.15},28:{L:0.20,M:1124,S:0.15},29:{L:0.19,M:1272,S:0.14}, - 30:{L:0.18,M:1430,S:0.14},31:{L:0.17,M:1607,S:0.14},32:{L:0.15,M:1795,S:0.14},33:{L:0.13,M:2008,S:0.13}, - 34:{L:0.12,M:2230,S:0.13},35:{L:0.10,M:2467,S:0.13},36:{L:0.08,M:2710,S:0.13},37:{L:0.06,M:2948,S:0.12}, - 38:{L:0.04,M:3195,S:0.12},39:{L:0.02,M:3380,S:0.12},40:{L:0.01,M:3530,S:0.12},41:{L:0.00,M:3660,S:0.12}, - 42:{L:-0.02,M:3820,S:0.12} - }, - female: { - 22:{L:0.23,M:474,S:0.17},23:{L:0.22,M:538,S:0.17},24:{L:0.22,M:610,S:0.17},25:{L:0.22,M:705,S:0.16}, - 26:{L:0.22,M:810,S:0.16},27:{L:0.21,M:920,S:0.15},28:{L:0.21,M:1040,S:0.15},29:{L:0.20,M:1178,S:0.14}, - 30:{L:0.19,M:1330,S:0.14},31:{L:0.18,M:1500,S:0.14},32:{L:0.16,M:1680,S:0.14},33:{L:0.14,M:1880,S:0.13}, - 34:{L:0.12,M:2090,S:0.13},35:{L:0.10,M:2310,S:0.13},36:{L:0.08,M:2540,S:0.13},37:{L:0.06,M:2766,S:0.12}, - 38:{L:0.04,M:3000,S:0.12},39:{L:0.02,M:3180,S:0.12},40:{L:0.01,M:3340,S:0.12},41:{L:0.00,M:3480,S:0.12}, - 42:{L:-0.02,M:3630,S:0.12} - } - }; - - function interpolateLMS(table, val) { - var keys = Object.keys(table).map(Number).sort(function(a,b){return a-b;}); - if (val <= keys[0]) return table[keys[0]]; - if (val >= keys[keys.length-1]) return table[keys[keys.length-1]]; - for (var i = 0; i < keys.length - 1; i++) { - if (val >= keys[i] && val <= keys[i+1]) { - var t = (val - keys[i]) / (keys[i+1] - keys[i]); - var a = table[keys[i]], b = table[keys[i+1]]; - return { L: a.L + t*(b.L-a.L), M: a.M + t*(b.M-a.M), S: a.S + t*(b.S-a.S) }; - } - } - return table[keys[0]]; - } - - function calcZ(value, L, M, S) { - if (Math.abs(L) < 0.001) return Math.log(value / M) / S; - return (Math.pow(value / M, L) - 1) / (L * S); - } - - function zToPercentile(z) { - // Approximation of the standard normal CDF - var a1=0.254829592, a2=-0.284496736, a3=1.421413741, a4=-1.453152027, a5=1.061405429, p=0.3275911; - var sign = z < 0 ? -1 : 1; - var x = Math.abs(z) / Math.sqrt(2); - var t = 1 / (1 + p * x); - var y = 1 - (((((a5*t + a4)*t) + a3)*t + a2)*t + a1)*t * Math.exp(-x*x); - return Math.round(((1 + sign * y) / 2) * 1000) / 10; - } - - function classifyGA(weeks, days) { - var total = weeks + (days || 0) / 7; - if (total < 28) return { label: 'Extremely Preterm', color: '#dc2626', icon: 'fa-triangle-exclamation' }; - if (total < 32) return { label: 'Very Preterm', color: '#ea580c', icon: 'fa-triangle-exclamation' }; - if (total < 34) return { label: 'Moderate Preterm', color: '#d97706', icon: 'fa-circle-exclamation' }; - if (total < 37) return { label: 'Late Preterm', color: '#ca8a04', icon: 'fa-circle-info' }; - if (total < 39) return { label: 'Early Term', color: '#2563eb', icon: 'fa-circle-info' }; - if (total < 41) return { label: 'Full Term', color: '#16a34a', icon: 'fa-circle-check' }; - if (total < 42) return { label: 'Late Term', color: '#d97706', icon: 'fa-circle-info' }; - return { label: 'Post Term', color: '#dc2626', icon: 'fa-triangle-exclamation' }; - } - - function classifyWeight(percentile) { - if (percentile < 3) return { label: 'Severely SGA', color: '#dc2626', detail: '<3rd percentile' }; - if (percentile < 10) return { label: 'SGA', color: '#ea580c', detail: '<10th percentile' }; - if (percentile > 97) return { label: 'Severely LGA', color: '#dc2626', detail: '>97th percentile' }; - if (percentile > 90) return { label: 'LGA', color: '#ea580c', detail: '>90th percentile' }; - return { label: 'AGA', color: '#16a34a', detail: '10th-90th percentile' }; - } - - function classifyBirthWeight(grams) { - if (grams < 1000) return { label: 'Extremely Low Birth Weight (ELBW)', color: '#dc2626' }; - if (grams < 1500) return { label: 'Very Low Birth Weight (VLBW)', color: '#ea580c' }; - if (grams < 2500) return { label: 'Low Birth Weight (LBW)', color: '#d97706' }; - if (grams <= 4000) return { label: 'Normal Birth Weight', color: '#16a34a' }; - return { label: 'Macrosomia (>4000g)', color: '#ea580c' }; - } - function neoAssess() { var weeks = parseInt(document.getElementById('neo-ga-weeks').value); var days = parseInt(document.getElementById('neo-ga-days').value) || 0; @@ -2183,6 +1831,10 @@ initImageLightbox(); resultDiv.innerHTML = '

Enter GA and birth weight.

'; return; } + if (!fentonLMS[sex] || Object.keys(fentonLMS[sex]).length === 0) { + resultDiv.innerHTML = '

Fenton growth data is still loading.

'; + return; + } var gaDecimal = weeks + days / 7; var gaClass = classifyGA(weeks, days); @@ -2190,7 +1842,7 @@ initImageLightbox(); // Calculate percentile using Fenton LMS var lms = interpolateLMS(fentonLMS[sex], gaDecimal); - var z = calcZ(weight, lms.L, lms.M, lms.S); + var z = calcNeonatalZ(weight, lms.L, lms.M, lms.S); var percentile = zToPercentile(z); var wtClass = classifyWeight(percentile); var expectedWeight = Math.round(lms.M); diff --git a/public/js/calculators/equipment.js b/public/js/calculators/equipment.js new file mode 100644 index 0000000..065ca0c --- /dev/null +++ b/public/js/calculators/equipment.js @@ -0,0 +1,38 @@ +var equipmentCache = null; + +export function loadEquipmentData() { + if (equipmentCache) return Promise.resolve(equipmentCache); + return fetch('/data/calculators/equipment.json', { credentials: 'same-origin' }) + .then(function(response) { + if (!response.ok) throw new Error('Failed to load equipment data'); + return response.json(); + }) + .then(function(data) { + equipmentCache = data.ageGroups || {}; + return equipmentCache; + }); +} + +export function renderEquipmentResult(eq) { + return '' + + '
' + + '
' + eq.label + '
' + + '
' + + '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
Bag-Valve Mask' + eq.bvm + '
Oral Airway' + eq.oral + '
Nasal Airway' + eq.nasal + '
Laryngoscope Blade' + eq.blade + '
ETT Size' + eq.ett + '
LMA Size' + eq.lma + '
Glidescope' + eq.glidescope + '
IV Catheter' + eq.iv + '
Central Line' + eq.cvl + '
NGT / OGT' + eq.ngt + '
Chest Tube' + eq.chest + '
Foley Catheter' + eq.foley + '
' + + '
' + + '
ETT Formulas: Uncuffed = (age/4)+4, Cuffed = (age/4)+3, Depth = ETT size x 3. Source: Harriet Lane Handbook.
'; +} diff --git a/public/js/calculators/neonatal.js b/public/js/calculators/neonatal.js new file mode 100644 index 0000000..0cac773 --- /dev/null +++ b/public/js/calculators/neonatal.js @@ -0,0 +1,70 @@ +var fentonCache = null; + +export function loadFentonLmsData() { + if (fentonCache) return Promise.resolve(fentonCache); + return fetch('/data/calculators/fenton-lms.json', { credentials: 'same-origin' }) + .then(function(response) { + if (!response.ok) throw new Error('Failed to load Fenton LMS data'); + return response.json(); + }) + .then(function(data) { + fentonCache = data.weightLms || {}; + return fentonCache; + }); +} + +export function interpolateLMS(table, val) { + var keys = Object.keys(table).map(Number).sort(function(a,b){return a-b;}); + if (val <= keys[0]) return table[keys[0]]; + if (val >= keys[keys.length-1]) return table[keys[keys.length-1]]; + for (var i = 0; i < keys.length - 1; i++) { + if (val >= keys[i] && val <= keys[i+1]) { + var t = (val - keys[i]) / (keys[i+1] - keys[i]); + var a = table[keys[i]], b = table[keys[i+1]]; + return { L: a.L + t*(b.L-a.L), M: a.M + t*(b.M-a.M), S: a.S + t*(b.S-a.S) }; + } + } + return table[keys[0]]; +} + +export function calcZ(value, L, M, S) { + if (Math.abs(L) < 0.001) return Math.log(value / M) / S; + return (Math.pow(value / M, L) - 1) / (L * S); +} + +export function zToPercentile(z) { + var a1=0.254829592, a2=-0.284496736, a3=1.421413741, a4=-1.453152027, a5=1.061405429, p=0.3275911; + var sign = z < 0 ? -1 : 1; + var x = Math.abs(z) / Math.sqrt(2); + var t = 1 / (1 + p * x); + var y = 1 - (((((a5*t + a4)*t) + a3)*t + a2)*t + a1)*t * Math.exp(-x*x); + return Math.round(((1 + sign * y) / 2) * 1000) / 10; +} + +export function classifyGA(weeks, days) { + var total = weeks + (days || 0) / 7; + if (total < 28) return { label: 'Extremely Preterm', color: '#dc2626', icon: 'fa-triangle-exclamation' }; + if (total < 32) return { label: 'Very Preterm', color: '#ea580c', icon: 'fa-triangle-exclamation' }; + if (total < 34) return { label: 'Moderate Preterm', color: '#d97706', icon: 'fa-circle-exclamation' }; + if (total < 37) return { label: 'Late Preterm', color: '#ca8a04', icon: 'fa-circle-info' }; + if (total < 39) return { label: 'Early Term', color: '#2563eb', icon: 'fa-circle-info' }; + if (total < 41) return { label: 'Full Term', color: '#16a34a', icon: 'fa-circle-check' }; + if (total < 42) return { label: 'Late Term', color: '#d97706', icon: 'fa-circle-info' }; + return { label: 'Post Term', color: '#dc2626', icon: 'fa-triangle-exclamation' }; +} + +export function classifyWeight(percentile) { + if (percentile < 3) return { label: 'Severely SGA', color: '#dc2626', detail: '<3rd percentile' }; + if (percentile < 10) return { label: 'SGA', color: '#ea580c', detail: '<10th percentile' }; + if (percentile > 97) return { label: 'Severely LGA', color: '#dc2626', detail: '>97th percentile' }; + if (percentile > 90) return { label: 'LGA', color: '#ea580c', detail: '>90th percentile' }; + return { label: 'AGA', color: '#16a34a', detail: '10th-90th percentile' }; +} + +export function classifyBirthWeight(grams) { + if (grams < 1000) return { label: 'Extremely Low Birth Weight (ELBW)', color: '#dc2626' }; + if (grams < 1500) return { label: 'Very Low Birth Weight (VLBW)', color: '#ea580c' }; + if (grams < 2500) return { label: 'Low Birth Weight (LBW)', color: '#d97706' }; + if (grams <= 4000) return { label: 'Normal Birth Weight', color: '#16a34a' }; + return { label: 'Macrosomia (>4000g)', color: '#ea580c' }; +} diff --git a/public/js/calculators/resus.js b/public/js/calculators/resus.js new file mode 100644 index 0000000..4246ddf --- /dev/null +++ b/public/js/calculators/resus.js @@ -0,0 +1,101 @@ +var resusCache = null; + +export function loadResusMedsData() { + if (resusCache) return Promise.resolve(resusCache); + return fetch('/data/calculators/resus-meds.json', { credentials: 'same-origin' }) + .then(function(response) { + if (!response.ok) throw new Error('Failed to load resus meds data'); + return response.json(); + }) + .then(function(data) { + resusCache = data; + return resusCache; + }); +} + +export function calculateResusDose(formula, w) { + var d, d1, d2, d3, ett, grams, detail, full, im, iv, ivVol, partial; + switch (formula) { + case 'adenosine': + d1 = +(w * 0.1).toFixed(2); + d2 = +(w * 0.2).toFixed(2); + d3 = +(w * 0.3).toFixed(2); + return { dose: d1 + ' mg (0.1 mg/kg)', extra: 'May repeat: ' + Math.min(d2, 12) + ' mg (0.2 mg/kg), then ' + Math.min(d3, 12) + ' mg (0.3 mg/kg)', max: 'Max first dose 6 mg, max subsequent 12 mg' }; + case 'amiodarone': + d = +(w * 5).toFixed(1); + return { dose: Math.min(d, 300) + ' mg (5 mg/kg)', extra: 'No pulse: push undiluted. Pulse: over 20-60 min. Subsequent max 150 mg.', max: 'Max first 300 mg, max total 15 mg/kg/24hr or 2200 mg' }; + case 'atropine': + d = +(w * 0.02).toFixed(3); + ett = (w * 0.04).toFixed(3) + '-' + (w * 0.06).toFixed(3); + return { dose: Math.min(d, 0.5) + ' mg (0.02 mg/kg)', extra: 'ETT dose: ' + ett + ' mg (0.04-0.06 mg/kg)', max: 'Max single 0.5 mg, max total 1 mg' }; + case 'calciumChloride': + d = +(w * 20).toFixed(0); + return { dose: Math.min(d, 1000) + ' mg (20 mg/kg)', extra: 'Give slowly. Central line preferred.', max: 'Max 1 g (1000 mg)' }; + case 'calciumGluconate': + d = +(w * 60).toFixed(0); + return { dose: Math.min(d, 3000) + ' mg (60 mg/kg)', extra: 'Give slowly over 10-20 min with cardiac monitoring.', max: 'Max 3 g (3000 mg)' }; + case 'dextrose': + grams = +(w * 0.5).toFixed(1) + '-' + +(w * 1).toFixed(1); + if (w < 5) detail = 'D10W: ' + (w * 5).toFixed(1) + '-' + (w * 10).toFixed(1) + ' mL (5-10 mL/kg)'; + else if (w < 45) detail = 'D25W: ' + (w * 2).toFixed(1) + '-' + (w * 4).toFixed(1) + ' mL (2-4 mL/kg)'; + else detail = 'D50W: ' + (w * 1).toFixed(1) + '-' + (w * 2).toFixed(1) + ' mL (1-2 mL/kg)'; + return { dose: grams + ' g (0.5-1 g/kg)', extra: detail, max: 'Max 25 g' }; + case 'epinephrine': + iv = +(w * 0.01).toFixed(3); + ivVol = +(w * 0.1).toFixed(2); + ett = +(w * 0.1).toFixed(2); + im = +(w * 0.01).toFixed(3); + return { dose: Math.min(iv, 1) + ' mg IV/IO (0.01 mg/kg of 0.1 mg/mL = ' + Math.min(ivVol, 10) + ' mL) q3-5 min', extra: 'ETT: ' + Math.min(ett, 2.5) + ' mg (0.1 mg/kg of 1 mg/mL). Anaphylaxis IM: ' + Math.min(im, 0.5) + ' mg (0.01 mg/kg)', max: 'Max IV 1 mg, max ETT 2.5 mg, max IM 0.5 mg' }; + case 'hydrocortisone': + d = +(w * 2).toFixed(1); + return { dose: Math.min(d, 100) + ' mg (2 mg/kg)', extra: 'Stress dosing for adrenal insufficiency.', max: 'Max 100 mg' }; + case 'insulin': + d = +(w * 0.1).toFixed(2); + return { dose: Math.min(d, 5) + ' units (0.1 units/kg)', extra: 'Give with ' + +(w * 0.5).toFixed(1) + ' g/kg dextrose (0.5 g/kg). Monitor glucose closely.', max: 'Max 5 units' }; + case 'lidocaine': + d = +(w * 1).toFixed(1); + ett = (w * 2).toFixed(1) + '-' + (w * 3).toFixed(1); + return { dose: Math.min(d, 100) + ' mg (1 mg/kg)', extra: 'ETT: ' + ett + ' mg (2-3 mg/kg). May repeat q5 min.', max: 'Max 100 mg/dose, max total 3 mg/kg' }; + case 'magnesiumSulfate': + d = +(w * 50).toFixed(0); + return { dose: Math.min(d, 2000) + ' mg (50 mg/kg)', extra: 'Give over 10-20 min (faster if pulseless).', max: 'Max 2 g (2000 mg)' }; + case 'naloxone': + partial = +(w * 0.001).toFixed(4) + '-' + +(w * 0.005).toFixed(4); + full = +(w * 0.1).toFixed(3); + return { dose: 'Partial: ' + partial + ' mg (0.001-0.005 mg/kg)', extra: 'Full reversal: ' + Math.min(full, 2) + ' mg (0.1 mg/kg)', max: 'Max partial first dose 0.1 mg, max full 2 mg' }; + case 'sodiumBicarbonate': + d = +(w * 1).toFixed(1); + return { dose: Math.min(d, 50) + ' mEq (1 mEq/kg)', extra: w < 10 ? 'Dilute to 0.5 mEq/mL (use 4.2% solution) for neonates/small infants.' : 'Use 8.4% solution (1 mEq/mL).', max: 'Max 50 mEq' }; + } + return { dose: '', extra: '', max: '' }; +} + +export function renderResusMeds(data, weightKg) { + var categories = data.categories || {}; + var html = '
Doses for ' + weightKg + ' kg patient
'; + html += '
'; + Object.keys(categories).forEach(function(cat) { + html += '' + categories[cat].label + ''; + }); + html += '
'; + html += '
'; + (data.medications || []).forEach(function(med) { + var r = calculateResusDose(med.formula, weightKg); + var color = categories[med.category] ? categories[med.category].color : '#6b7280'; + html += '
' + + '
' + + '
' + med.name + '
' + + '
' + med.indication + '
' + + '
' + + '
' + + '
Dose: ' + r.dose + '
' + + '
' + r.extra + '
' + + '
Max: ' + r.max + '
' + + '
Route: ' + med.route + '
' + + '
' + + '
'; + }); + html += '
'; + html += '
Disclaimer: Always verify doses against institutional protocols and current guidelines. This tool is for educational reference only.
'; + return html; +} diff --git a/test/calculators-data.test.js b/test/calculators-data.test.js index dadd7cb..61f7171 100644 --- a/test/calculators-data.test.js +++ b/test/calculators-data.test.js @@ -45,3 +45,84 @@ test('vitals.json entries have complete renderable fields', () => { assert.ok(Array.isArray(group.notes) && group.notes.length > 0, `${key}.notes`); }); }); + +test('resus-meds.json exposes all PALS medication formulas', () => { + const raw = fs.readFileSync(path.join(__dirname, '..', 'public', 'data', 'calculators', 'resus-meds.json'), 'utf8'); + const data = JSON.parse(raw); + assert.equal(data.version, '1.0'); + assert.deepEqual(data.medications.map((m) => m.formula), [ + 'adenosine', + 'amiodarone', + 'atropine', + 'calciumChloride', + 'calciumGluconate', + 'dextrose', + 'epinephrine', + 'hydrocortisone', + 'insulin', + 'lidocaine', + 'magnesiumSulfate', + 'naloxone', + 'sodiumBicarbonate' + ]); +}); + +test('resus dose formulas preserve representative outputs', async () => { + const { calculateResusDose } = await import('../public/js/calculators/resus.js'); + assert.equal(calculateResusDose('adenosine', 20).dose, '2 mg (0.1 mg/kg)'); + assert.equal(calculateResusDose('epinephrine', 10).dose, '0.1 mg IV/IO (0.01 mg/kg of 0.1 mg/mL = 1 mL) q3-5 min'); + assert.equal(calculateResusDose('magnesiumSulfate', 50).dose, '2000 mg (50 mg/kg)'); + assert.equal(calculateResusDose('sodiumBicarbonate', 8).extra, 'Dilute to 0.5 mEq/mL (use 4.2% solution) for neonates/small infants.'); +}); + +test('fenton-lms.json preserves key LMS anchors', () => { + const raw = fs.readFileSync(path.join(__dirname, '..', 'public', 'data', 'calculators', 'fenton-lms.json'), 'utf8'); + const data = JSON.parse(raw); + assert.equal(data.version, '1.0'); + assert.deepEqual(data.weightLms.male['22'], { L: 0.21, M: 496, S: 0.17 }); + assert.deepEqual(data.weightLms.male['40'], { L: 0.01, M: 3530, S: 0.12 }); + assert.deepEqual(data.weightLms.female['22'], { L: 0.23, M: 474, S: 0.17 }); + assert.deepEqual(data.weightLms.female['40'], { L: 0.01, M: 3340, S: 0.12 }); +}); + +test('neonatal Fenton helpers preserve interpolation and classification', async () => { + const neonatal = await import('../public/js/calculators/neonatal.js'); + const raw = fs.readFileSync(path.join(__dirname, '..', 'public', 'data', 'calculators', 'fenton-lms.json'), 'utf8'); + const data = JSON.parse(raw).weightLms; + assert.deepEqual(neonatal.interpolateLMS(data.male, 30), { L: 0.18, M: 1430, S: 0.14 }); + assert.equal(Math.round(neonatal.interpolateLMS(data.female, 30.5).M), 1415); + assert.equal(neonatal.classifyGA(36, 6).label, 'Late Preterm'); + assert.equal(neonatal.classifyBirthWeight(900).label, 'Extremely Low Birth Weight (ELBW)'); + assert.equal(neonatal.classifyWeight(50).label, 'AGA'); +}); + +test('equipment.json preserves equipment age groups and anchors', () => { + const raw = fs.readFileSync(path.join(__dirname, '..', 'public', 'data', 'calculators', 'equipment.json'), 'utf8'); + const data = JSON.parse(raw); + assert.equal(data.version, '1.0'); + assert.deepEqual(Object.keys(data.ageGroups), [ + 'premie', + 'newborn', + '6mo', + '1yr', + '2-3yr', + '4-6yr', + '7-10yr', + '11-15yr', + '16yr' + ]); + assert.equal(data.ageGroups.newborn.ett, '3.0-3.5'); + assert.equal(data.ageGroups['1yr'].lma, '2'); + assert.equal(data.ageGroups['4-6yr'].blade, 'Miller 2 / MAC 2'); + assert.equal(data.ageGroups['16yr'].foley, '12 Fr'); +}); + +test('equipment.json entries have complete renderable fields', () => { + const raw = fs.readFileSync(path.join(__dirname, '..', 'public', 'data', 'calculators', 'equipment.json'), 'utf8'); + const groups = JSON.parse(raw).ageGroups; + Object.entries(groups).forEach(([key, group]) => { + ['label', 'bvm', 'nasal', 'oral', 'blade', 'ett', 'lma', 'glidescope', 'iv', 'cvl', 'ngt', 'chest', 'foley'].forEach((field) => { + assert.ok(group[field], `${key}.${field}`); + }); + }); +});