extract calculator reference data
This commit is contained in:
parent
3c4ca84b5f
commit
6a081bb53b
8 changed files with 405 additions and 372 deletions
15
public/data/calculators/equipment.json
Normal file
15
public/data/calculators/equipment.json
Normal 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" }
|
||||
}
|
||||
}
|
||||
52
public/data/calculators/fenton-lms.json
Normal file
52
public/data/calculators/fenton-lms.json
Normal 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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
24
public/data/calculators/resus-meds.json
Normal file
24
public/data/calculators/resus-meds.json
Normal 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" }
|
||||
]
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
38
public/js/calculators/equipment.js
Normal file
38
public/js/calculators/equipment.js
Normal 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>';
|
||||
}
|
||||
70
public/js/calculators/neonatal.js
Normal file
70
public/js/calculators/neonatal.js
Normal 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' };
|
||||
}
|
||||
101
public/js/calculators/resus.js
Normal file
101
public/js/calculators/resus.js
Normal 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;
|
||||
}
|
||||
|
|
@ -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}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue