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 =
- '' +
- '' +
- '
' +
- '' +
- '| 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 ''
+ + ''
+ + ''
+ + '
'
+ + '| 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}`);
+ });
+ });
+});