// 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 (
);
}
// ─── 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"
/>
Tipo de apoyo
setP({ apoyo: e.target.value })}
>
Estable · 400
Puntas nasales · 500
Fiebre · 600
Ventilador mecánico · 700
Calor radiante · 300
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 de temperatura
setTempBucket(e.target.value)}>
Auto · según °C ingresada
< 37 °C · ×0.5
37–38 °C · ×0.6
38–39 °C · ×0.7
> 39 °C · ×1.0
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"/>
Equipo
{[
['macro', 'Macrogotero · 10 gtt/ml'],
['normo', 'Normogotero · 20 gtt/ml'],
['micro', 'Microgotero · 60 gtt/ml'],
['trans', 'Transfusión · 15 gtt/ml'],
].map(([k, l]) => (
setEquipo(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.50 Sobrecarga / inadecuado
0.50 – 0.70 Normal
0.70 – 1.00 Choque leve
1.00 – 1.40 Choque moderado
≥ 1.40 Choque 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.
Volumen Horas ml / hora Gotas / minuto
{rows.map((r, i) => (
{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,
};