extract calculator reference data

This commit is contained in:
Daniel 2026-05-08 03:18:37 +02:00
parent 3c4ca84b5f
commit 6a081bb53b
8 changed files with 405 additions and 372 deletions

View file

@ -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" }
}
}

View file

@ -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 }
}
}
}

View file

@ -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" }
]
}

View file

@ -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 = '<div style="margin-bottom:12px;font-size:14px;font-weight:700;color:var(--g800);">Doses for ' + w + ' kg patient</div>';
html += '<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:14px;font-size:11px;">';
Object.keys(categoryColors).forEach(function(cat) {
html += '<span style="display:inline-flex;align-items:center;gap:4px;"><span style="width:10px;height:10px;border-radius:50%;background:' + categoryColors[cat] + ';display:inline-block;"></span>' + categoryLabels[cat] + '</span>';
});
html += '</div>';
html += '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:12px;">';
RESUS_MEDS.forEach(function(med) {
var r = med.calc(w);
var color = categoryColors[med.category] || '#6b7280';
html += '<div style="border:1.5px solid ' + color + '33;border-radius:10px;overflow:hidden;background:#fff;">' +
'<div style="padding:10px 14px;background:' + color + '0d;border-bottom:1px solid ' + color + '22;">' +
'<div style="font-size:14px;font-weight:700;color:' + color + ';">' + med.name + '</div>' +
'<div style="font-size:11px;color:var(--g500);margin-top:2px;">' + med.indication + '</div>' +
'</div>' +
'<div style="padding:12px 14px;font-size:13px;">' +
'<div style="margin-bottom:6px;"><strong>Dose:</strong> ' + r.dose + '</div>' +
'<div style="margin-bottom:6px;font-size:12px;color:var(--g600);">' + r.extra + '</div>' +
'<div style="font-size:11px;color:var(--g500);"><strong>Max:</strong> ' + r.max + '</div>' +
'<div style="font-size:11px;color:var(--g400);margin-top:4px;"><strong>Route:</strong> ' + med.route + '</div>' +
'</div>' +
'</div>';
});
html += '</div>';
html += '<div style="margin-top:14px;padding:10px;background:#fef3c7;border-radius:8px;font-size:11px;color:#92400e;">' +
'<strong>Disclaimer:</strong> Always verify doses against institutional protocols and current guidelines. This tool is for educational reference only.' +
'</div>';
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 =
'<div class="calc-result-header" style="background:#eff6ff;border-color:#2563eb;">' +
'<div style="font-size:18px;font-weight:700;color:#1d4ed8;">' + eq.label + '</div>' +
'</div>' +
'<div style="margin-top:12px;overflow-x:auto;">' +
'<table class="admin-table" style="font-size:13px;width:100%;">' +
'<tbody>' +
'<tr><td style="font-weight:600;width:40%;">Bag-Valve Mask</td><td>' + eq.bvm + '</td></tr>' +
'<tr><td style="font-weight:600;">Oral Airway</td><td>' + eq.oral + '</td></tr>' +
'<tr><td style="font-weight:600;">Nasal Airway</td><td>' + eq.nasal + '</td></tr>' +
'<tr><td style="font-weight:600;">Laryngoscope Blade</td><td>' + eq.blade + '</td></tr>' +
'<tr><td style="font-weight:600;">ETT Size</td><td>' + eq.ett + '</td></tr>' +
'<tr><td style="font-weight:600;">LMA Size</td><td>' + eq.lma + '</td></tr>' +
'<tr><td style="font-weight:600;">Glidescope</td><td>' + eq.glidescope + '</td></tr>' +
'<tr><td style="font-weight:600;">IV Catheter</td><td>' + eq.iv + '</td></tr>' +
'<tr><td style="font-weight:600;">Central Line</td><td>' + eq.cvl + '</td></tr>' +
'<tr><td style="font-weight:600;">NGT / OGT</td><td>' + eq.ngt + '</td></tr>' +
'<tr><td style="font-weight:600;">Chest Tube</td><td>' + eq.chest + '</td></tr>' +
'<tr><td style="font-weight:600;">Foley Catheter</td><td>' + eq.foley + '</td></tr>' +
'</tbody>' +
'</table>' +
'</div>' +
'<div style="margin-top:10px;font-size:11px;color:var(--g400);">' +
'<strong>ETT Formulas:</strong> Uncuffed = (age/4)+4, Cuffed = (age/4)+3, Depth = ETT size x 3. ' +
'Source: Harriet Lane Handbook.' +
'</div>';
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 = '<p style="color:var(--red);font-size:13px;">Enter GA and birth weight.</p>';
return;
}
if (!fentonLMS[sex] || Object.keys(fentonLMS[sex]).length === 0) {
resultDiv.innerHTML = '<p style="color:var(--red);font-size:13px;">Fenton growth data is still loading.</p>';
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);

View file

@ -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 ''
+ '<div class="calc-result-header" style="background:#eff6ff;border-color:#2563eb;">'
+ '<div style="font-size:18px;font-weight:700;color:#1d4ed8;">' + eq.label + '</div>'
+ '</div>'
+ '<div style="margin-top:12px;overflow-x:auto;">'
+ '<table class="admin-table" style="font-size:13px;width:100%;"><tbody>'
+ '<tr><td style="font-weight:600;width:40%;">Bag-Valve Mask</td><td>' + eq.bvm + '</td></tr>'
+ '<tr><td style="font-weight:600;">Oral Airway</td><td>' + eq.oral + '</td></tr>'
+ '<tr><td style="font-weight:600;">Nasal Airway</td><td>' + eq.nasal + '</td></tr>'
+ '<tr><td style="font-weight:600;">Laryngoscope Blade</td><td>' + eq.blade + '</td></tr>'
+ '<tr><td style="font-weight:600;">ETT Size</td><td>' + eq.ett + '</td></tr>'
+ '<tr><td style="font-weight:600;">LMA Size</td><td>' + eq.lma + '</td></tr>'
+ '<tr><td style="font-weight:600;">Glidescope</td><td>' + eq.glidescope + '</td></tr>'
+ '<tr><td style="font-weight:600;">IV Catheter</td><td>' + eq.iv + '</td></tr>'
+ '<tr><td style="font-weight:600;">Central Line</td><td>' + eq.cvl + '</td></tr>'
+ '<tr><td style="font-weight:600;">NGT / OGT</td><td>' + eq.ngt + '</td></tr>'
+ '<tr><td style="font-weight:600;">Chest Tube</td><td>' + eq.chest + '</td></tr>'
+ '<tr><td style="font-weight:600;">Foley Catheter</td><td>' + eq.foley + '</td></tr>'
+ '</tbody></table>'
+ '</div>'
+ '<div style="margin-top:10px;font-size:11px;color:var(--g400);"><strong>ETT Formulas:</strong> Uncuffed = (age/4)+4, Cuffed = (age/4)+3, Depth = ETT size x 3. Source: Harriet Lane Handbook.</div>';
}

View file

@ -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' };
}

View file

@ -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 = '<div style="margin-bottom:12px;font-size:14px;font-weight:700;color:var(--g800);">Doses for ' + weightKg + ' kg patient</div>';
html += '<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:14px;font-size:11px;">';
Object.keys(categories).forEach(function(cat) {
html += '<span style="display:inline-flex;align-items:center;gap:4px;"><span style="width:10px;height:10px;border-radius:50%;background:' + categories[cat].color + ';display:inline-block;"></span>' + categories[cat].label + '</span>';
});
html += '</div>';
html += '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:12px;">';
(data.medications || []).forEach(function(med) {
var r = calculateResusDose(med.formula, weightKg);
var color = categories[med.category] ? categories[med.category].color : '#6b7280';
html += '<div style="border:1.5px solid ' + color + '33;border-radius:10px;overflow:hidden;background:#fff;">' +
'<div style="padding:10px 14px;background:' + color + '0d;border-bottom:1px solid ' + color + '22;">' +
'<div style="font-size:14px;font-weight:700;color:' + color + ';">' + med.name + '</div>' +
'<div style="font-size:11px;color:var(--g500);margin-top:2px;">' + med.indication + '</div>' +
'</div>' +
'<div style="padding:12px 14px;font-size:13px;">' +
'<div style="margin-bottom:6px;"><strong>Dose:</strong> ' + r.dose + '</div>' +
'<div style="margin-bottom:6px;font-size:12px;color:var(--g600);">' + r.extra + '</div>' +
'<div style="font-size:11px;color:var(--g500);"><strong>Max:</strong> ' + r.max + '</div>' +
'<div style="font-size:11px;color:var(--g400);margin-top:4px;"><strong>Route:</strong> ' + med.route + '</div>' +
'</div>' +
'</div>';
});
html += '</div>';
html += '<div style="margin-top:14px;padding:10px;background:#fef3c7;border-radius:8px;font-size:11px;color:#92400e;"><strong>Disclaimer:</strong> Always verify doses against institutional protocols and current guidelines. This tool is for educational reference only.</div>';
return html;
}

View file

@ -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}`);
});
});
});