// Calculators — individual calculator components. // All loaded under window.Calc.* for the Tools section to consume. // Each calculator self-contains: inputs, computation, results card. const { useState: useCS, useMemo: useCM, useEffect: useCE } = React; // ─── Shared utilities ─────────────────────────────────────── const fmt = (n, d = 1) => (isFinite(n) ? n.toFixed(d) : '—'); const fmt0 = (n) => (isFinite(n) ? Math.round(n).toString() : '—'); // ─── Shared patient store (localStorage-backed) ───────────── // Pulled into PatientPanel and also referenced by individual calculators // when a patient profile is active. const PATIENT_KEY = 'cyna_patient_v1'; const defaultPatient = () => ({ poblacion: 'adulto', // 'adulto' | 'pediatra' | 'neonato' nombre: '', edad: '', edadUnit: 'años', // 'años' | 'meses' | 'días' sexo: 'F', // 'F' | 'M' peso: '', // kg (gramos para prematuro) talla: '', // cm fc: '', // frecuencia cardiaca fr: '', // frecuencia respiratoria tas: '', // presión sistólica tad: '', // presión diastólica temp: '', // °C spo2: '', // % apoyo: 'estable', horas: 8, // Soluciones planificadas soluciones: [], // [{nombre, volumen, tiempo, equipo}] }); let patientState = defaultPatient(); try { const raw = localStorage.getItem(PATIENT_KEY); if (raw) patientState = { ...patientState, ...JSON.parse(raw) }; } catch {} const patientSubs = new Set(); function setPatient(updater) { patientState = typeof updater === 'function' ? { ...patientState, ...updater(patientState) } : { ...patientState, ...updater }; try { localStorage.setItem(PATIENT_KEY, JSON.stringify(patientState)); } catch {} patientSubs.forEach(fn => fn(patientState)); } function usePatient() { const [, setTick] = useCS(0); useCE(() => { const fn = () => setTick(t => t + 1); patientSubs.add(fn); return () => patientSubs.delete(fn); }, []); return [patientState, setPatient]; } window.PatientStore = { usePatient, setPatient, get: () => patientState }; // ─── Reusable bits ────────────────────────────────────────── function NumberField({ label, value, onChange, unit, step = 'any', min = '0', placeholder }) { return (
onChange(e.target.value === '' ? '' : +e.target.value)} step={step} min={min} placeholder={placeholder} /> {unit && {unit}}
); } // ─── 1. Pérdidas insensibles · Pediátrico (incl. neonatal) ── function PerdidasPediatricas() { const [p, setP] = usePatient(); const peso = +p.peso || 15; const horas = +p.horas || 8; const apoyo = p.apoyo || 'estable'; const [horasCustom, setHorasCustom] = useCS(8); // Tabla de constantes según tipo de apoyo const constantes = { estable: 400, puntas: 500, fiebre: 600, ventilador: 700, calor: 300 }; const C = constantes[apoyo] ?? 400; // Superficie corporal según peso const sc = peso <= 10 ? (peso * 4 + 9) / 100 : (peso * 4 + 7) / (90 + peso); const perHora = (sc * C) / 24; // mL/h const total24 = sc * C; // mL/24h const totalCustom = perHora * (+horasCustom || 0); return (

Pérdidas insensibles · Pediátrico / Neonatal

Fórmula: SC × constante = mL en 24 h · / 24 = mL/h.

setP({ peso: v })} unit="kg" step="0.1" placeholder="ej. 15" />
setHorasCustom(v)} unit="h" step="1" placeholder="ej. 8" />
Superficie corporal (SC) {fmt(sc, 3)} m²
En 1 hora
{fmt(perHora, 1)} ml
En {horasCustom || 0} h · solicitadas
{fmt(totalCustom, 1)} ml
En 24 horas
{fmt(total24, 1)} ml
SC: {peso <= 10 ? '(peso×4 + 9)/100' : '(peso×4 + 7)/(90 + peso)'} · Constante {C} · Pérdidas en 24 h = SC × {C}
); } // ─── 2. Pérdidas insensibles · Prematuro — Próximamente ───── function PerdidasPrematuros() { return (

Pérdidas insensibles · Prematuro

Cálculo basado en peso al nacimiento y edad postnatal.

Próximamente

Estamos validando la fórmula con fuentes confiables

La tabla de constantes para prematuros varía según peso, edad postnatal y condiciones ambientales (humedad de la incubadora, fototerapia, calor radiante). Estamos cotejando guías de la AAP y de la SEN para publicar una versión clínicamente sólida.

Mientras tanto, para neonatos de término puedes usar la calculadora pediátrica con peso real.

); } // ─── 3. Pérdidas insensibles · Adulto ─────────────────────── function PerdidasAdultos() { const [p, setP] = usePatient(); const peso = +p.peso || 70; const tempVal = +p.temp; const [horasCustom, setHorasCustom] = useCS(8); // Determine factor automatically by temperature if provided, else manual override const [tempBucket, setTempBucket] = useCS('auto'); const autoBucket = !isFinite(tempVal) || tempVal === 0 ? 'lt37' : tempVal < 37 ? 'lt37' : tempVal < 38 ? 't37' : tempVal < 39 ? 't38' : 'gt39'; const bucket = tempBucket === 'auto' ? autoBucket : tempBucket; const factors = { lt37: 0.5, t37: 0.6, t38: 0.7, gt39: 1 }; const factor = factors[bucket]; const perHora = peso * factor; const total24 = perHora * 24; const totalCustom = perHora * (+horasCustom || 0); const bucketLabel = { lt37: '< 37 °C', t37: '37–38 °C', t38: '38–39 °C', gt39: '> 39 °C' }[bucket]; return (

Pérdidas insensibles · Adulto

Fórmula: peso × factor = mL/h · multiplicado por las horas indicadas.

setP({ peso: v })} unit="kg" placeholder="ej. 70"/> setP({ temp: v })} unit="°C" step="0.1" placeholder="ej. 37.5"/> setHorasCustom(v)} unit="h" placeholder="ej. 12"/>
Rango aplicado · factor × {factor} {bucketLabel}
Consumido por hora
{fmt(perHora, 1)} ml
En {horasCustom || 0} h · solicitadas
{fmt(totalCustom, 1)} ml
En 24 horas
{fmt(total24, 1)} ml
Factores por temperatura · <37 °C: 0.5 · 37–38 °C: 0.6 · 38–39 °C: 0.7 · >39 °C: 1.0
); } // ─── 4. Factor goteo / Gotas por minuto ───────────────────── function FactorGoteo() { const [volumen, setVolumen] = useCS(500); const [tiempo, setTiempo] = useCS(60); // minutes const [equipo, setEquipo] = useCS('macro'); const gotasPorMl = { macro: 10, normo: 20, micro: 60, trans: 15 }[equipo]; const equipoName = { macro: 'Macrogotero', normo: 'Normogotero', micro: 'Microgotero', trans: 'Equipo de transfusión' }[equipo]; const gotasMin = (volumen * gotasPorMl) / tiempo; const K = 60 / gotasPorMl; // factor / constante const mlPorHora = volumen / (tiempo / 60); // velocidad de infusión const mlPorGota = 1 / gotasPorMl; // equivalencia ml/gota return (

Factor goteo · Velocidad de infusión

Fórmula: (volumen × factor de goteo) / tiempo · incluye equivalencias y mL/h.

setVolumen(v || 0)} unit="ml" step="50"/> setTiempo(v || 0)} unit="min"/>
{[ ['macro', 'Macrogotero · 10 gtt/ml'], ['normo', 'Normogotero · 20 gtt/ml'], ['micro', 'Microgotero · 60 gtt/ml'], ['trans', 'Transfusión · 15 gtt/ml'], ].map(([k, l]) => ( ))}
{equipoName} {gotasPorMl} gotas = 1 ml
Constante (K) 60 / {gotasPorMl} = {fmt(K, 2)}
Equivalencia por gota 1 gota = {fmt(mlPorGota, 3)} ml
Gotas por minuto
{fmt0(gotasMin)} gtt/min
Velocidad de infusión
{fmt0(mlPorHora)} ml/h
{volumen} ml ÷ {tiempo} min × {gotasPorMl} gtt/ml = {fmt0(gotasMin)} gtt/min · equivalente a {fmt0(mlPorHora)} ml/h
); } // ─── 5. IMC + clasificación OMS ──────────────────────────── const IMC_LEVELS = [ { max: 16, label: 'Delgadez severa', color: 'var(--wine-700)', tint: 'var(--wine-100)' }, { max: 16.99, label: 'Delgadez moderada', color: 'var(--wine-500)', tint: 'var(--wine-100)' }, { max: 18.49, label: 'Delgadez leve', color: 'var(--honey-700)', tint: 'var(--honey-100)' }, { max: 24.99, label: 'Peso normal', color: 'var(--sage-700)', tint: 'var(--sage-100)' }, { max: 29.99, label: 'Sobrepeso', color: 'var(--honey-700)', tint: 'var(--honey-100)' }, { max: 34.99, label: 'Obesidad grado I', color: 'var(--cinnamon-600)', tint: 'var(--cinnamon-100)' }, { max: 39.99, label: 'Obesidad grado II', color: 'var(--wine-500)', tint: 'var(--wine-100)' }, { max: Infinity, label: 'Obesidad grado III · mórbida', color: 'var(--wine-700)', tint: 'var(--wine-100)' }, ]; function classifyImc(imc) { if (!isFinite(imc) || imc <= 0) return null; return IMC_LEVELS.find(l => imc <= l.max); } function IMCalc() { const [p, setP] = usePatient(); const peso = +p.peso; const tallaCm = +p.talla; const tallaM = tallaCm / 100; const imc = (peso && tallaM) ? peso / (tallaM * tallaM) : NaN; const klass = classifyImc(imc); const pesoIdealMin = isFinite(tallaM) ? 18.5 * tallaM * tallaM : NaN; const pesoIdealMax = isFinite(tallaM) ? 24.9 * tallaM * tallaM : NaN; return (

Índice de Masa Corporal (IMC)

Fórmula: peso (kg) ÷ talla² (m) · Clasificación según OMS.

setP({ peso: v })} unit="kg" step="0.1" placeholder="ej. 70"/> setP({ talla: v })} unit="cm" placeholder="ej. 170"/>
IMC {fmt(imc, 2)} kg/m²
{klass && (
Clasificación OMS
{klass.label}
)}
{IMC_LEVELS.map((l, i) => (
{i === 0 ? '< 16.0' : l.max === Infinity ? '≥ 40.0' : `${IMC_LEVELS[i - 1] ? (IMC_LEVELS[i - 1].max + 0.01).toFixed(2) : '16.00'} – ${l.max.toFixed(2)}`} {l.label}
))}
{isFinite(pesoIdealMin) && (
Rango saludable para esta talla: {fmt(pesoIdealMin, 1)} – {fmt(pesoIdealMax, 1)} kg
)}
); } // ─── 6. Índice de choque (Shock Index) ────────────────────── function IndiceChoque() { const [p, setP] = usePatient(); const fc = +p.fc; const tas = +p.tas; const si = (fc && tas) ? fc / tas : NaN; // Bandas clínicas: <0.5 inadecuada; 0.5–0.7 normal; 0.7–1.0 leve; 1.0–1.4 moderado; >1.4 severo const level = !isFinite(si) ? null : si < 0.5 ? { label: 'Volumen inadecuado / sobrecarga', color: 'var(--wine-500)', tint: 'var(--wine-100)' } : si <= 0.7 ? { label: 'Normal', color: 'var(--sage-700)', tint: 'var(--sage-100)' } : si < 1.0 ? { label: 'Choque leve / compensación', color: 'var(--honey-700)', tint: 'var(--honey-100)' } : si < 1.4 ? { label: 'Choque moderado', color: 'var(--cinnamon-600)', tint: 'var(--cinnamon-100)' } : { label: 'Choque severo · alerta', color: 'var(--wine-700)', tint: 'var(--wine-100)' }; return (

Índice de choque (Shock Index)

Fórmula: frecuencia cardiaca ÷ presión sistólica · útil para sospecha temprana de choque.

setP({ fc: v })} unit="lpm" placeholder="ej. 96"/> setP({ tas: v })} unit="mmHg" placeholder="ej. 120"/>
Índice de choque {fmt(si, 2)}
{level && (
Interpretación
{level.label}
)}
< 0.50Sobrecarga / inadecuado
0.50 – 0.70Normal
0.70 – 1.00Choque leve
1.00 – 1.40Choque moderado
≥ 1.40Choque severo
); } // ─── 7. Presión arterial media (PAM / MAP) ────────────────── function PAM() { const [p, setP] = usePatient(); const tas = +p.tas; const tad = +p.tad; const pam = (tas && tad) ? (tas + 2 * tad) / 3 : NaN; const pp = (tas && tad) ? tas - tad : NaN; // presión de pulso const level = !isFinite(pam) ? null : pam < 60 ? { label: 'Hipoperfusión · riesgo de daño orgánico', color: 'var(--wine-700)', tint: 'var(--wine-100)' } : pam < 70 ? { label: 'Límite inferior', color: 'var(--honey-700)', tint: 'var(--honey-100)' } : pam <= 105? { label: 'Perfusión adecuada', color: 'var(--sage-700)', tint: 'var(--sage-100)' } : { label: 'Elevada · vigilar hipertensión', color: 'var(--wine-500)', tint: 'var(--wine-100)' }; return (

Presión arterial media (PAM)

Fórmula: (sistólica + 2 × diastólica) ÷ 3 · meta clínica habitual: ≥ 65 mmHg.

setP({ tas: v })} unit="mmHg" placeholder="ej. 120"/> setP({ tad: v })} unit="mmHg" placeholder="ej. 80"/>
Presión de pulso {fmt0(pp)} mmHg
PAM {fmt(pam, 1)} mmHg
{level && (
Interpretación
{level.label}
)}
Meta habitual en cuidados intensivos: PAM ≥ 65 mmHg para asegurar perfusión tisular.
); } // ─── 8. Tabla líquidos infundidos (referencia) ────────────── function TablaLiquidos() { const rows = [ [1000, 24, 41, 13], [1000, 12, 83, 27], [1000, 8, 125, 41], [1000, 6, 166, 55], [1000, 4, 250, 83], [500, 24, 20, 6], [500, 12, 41, 13], [500, 8, 62, 20], [500, 6, 83, 27], [500, 4, 125, 41], [250, 24, 10, 3], [250, 12, 20, 6], [250, 8, 31, 10], [250, 6, 41, 13], [250, 4, 62, 20], ]; return (

Líquidos infundidos · Tabla de referencia

Macrogotero (10 gtt/ml) — valores de mantenimiento más comunes.

{rows.map((r, i) => ( ))}
VolumenHorasml / horaGotas / minuto
{r[0]} ml {r[1]} h {r[2]} ml {r[3]} gtt
); } window.Calc = { PerdidasPediatricas, PerdidasPrematuros, PerdidasAdultos, FactorGoteo, IMCalc, IndiceChoque, PAM, TablaLiquidos, // exports for re-use fmt, fmt0, classifyImc, IMC_LEVELS, NumberField, };