import { useState, useCallback } from "react"; const COLORS = { teal: "#0D6E6E", tealLight: "#E6F4F4", tealMid: "#1A8F8F", navy: "#0B2545", slate: "#4A5568", muted: "#718096", border: "#CBD5E0", bg: "#F7FAFA", white: "#FFFFFF", green: "#276749", greenBg: "#F0FFF4", yellow: "#744210", yellowBg: "#FFFBEB", red: "#9B2335", redBg: "#FFF5F5", blue: "#1A4E8A", blueBg: "#EBF4FF", purple: "#553C9A", purpleBg: "#FAF5FF", }; const TABS = ["Dosing", "Peds", "Scoring", "General"]; // ─── UNIT HELPERS ───────────────────────────────────────────────────────────── const lbsToKg = (lbs) => parseFloat(lbs) / 2.205; const kgToLbs = (kg) => parseFloat(kg) * 2.205; const feetInchesToCm = (ft, inch) => (parseInt(ft || 0) * 12 + parseFloat(inch || 0)) * 2.54; // ─── SHARED STYLES ──────────────────────────────────────────────────────────── const labelStyle = { fontFamily:"'DM Sans',sans-serif", fontSize:13, fontWeight:600, color:COLORS.slate, display:"block" }; const inputStyle = { width:"100%", padding:"10px 12px", borderRadius:8, border:`1.5px solid ${COLORS.border}`, fontFamily:"'DM Sans',sans-serif", fontSize:15, color:COLORS.navy, outline:"none", boxSizing:"border-box", background:COLORS.bg }; const selectStyle = { padding:"10px 12px", borderRadius:8, border:`1.5px solid ${COLORS.border}`, fontFamily:"'DM Sans',sans-serif", fontSize:15, color:COLORS.navy, background:COLORS.white, cursor:"pointer" }; // ─── SMALL REUSABLES ───────────────────────────────────────────────────────── function Row({ label, value }) { return (
{label} {value}
); } function Stat({ label, value, sub }) { return (
{label}
{value}
{sub &&
{sub}
}
); } function RiskBadge({ level, risk, action }) { const map = { green:{ bg:COLORS.greenBg, color:COLORS.green, border:"#9AE6B4" }, yellow:{ bg:COLORS.yellowBg, color:COLORS.yellow, border:"#F6E05E" }, red:{ bg:COLORS.redBg, color:COLORS.red, border:"#FC8181" }, blue:{ bg:COLORS.blueBg, color:COLORS.blue, border:"#90CDF4" }, purple:{ bg:COLORS.purpleBg, color:COLORS.purple, border:"#D6BCFA" }, }; const s = map[level] || map.green; return (
{risk}
{action &&
{action}
}
); } function CalcShell({ title, subtitle, children }) { return (
{title}
{subtitle &&
{subtitle}
} {children}
); } function Field({ label, id, vals, set, placeholder }) { return (
set(id,e.target.value)} style={{...inputStyle, marginTop:6}} placeholder={placeholder||"0"} />
); } function HeightField({ ftId, inId, vals, set }) { return (
set(ftId,e.target.value)} style={{...inputStyle,paddingRight:28}} /> ft
set(inId,e.target.value)} style={{...inputStyle,paddingRight:28}} /> in
); } function SexToggle({ sexKey, vals, set }) { return (
{["male","female"].map(s=>( ))}
); } function ResultBox({ label, value, unit, badge, note }) { return (
{label}
{value} {unit}
{badge && } {note &&
{note}
}
); } function SubTabBar({ options, active, onSelect }) { return (
{options.map(o=>( ))}
); } // ══════════════════════════════════════════════════════════════════════════════ // DOSING DATA // ══════════════════════════════════════════════════════════════════════════════ const MEDICATIONS = [ { name:"Acetaminophen", indications:["Fever / Pain"], dosePerKg:15, maxDoseMg:1000, maxDailyMg:4000, frequency:"Every 4–6 hrs", form:"Liquid 160mg/5mL · Chewable 80mg · Tab 325mg", note:"Max 5 doses/day. Avoid in liver disease." }, { name:"Ibuprofen", indications:["Fever / Pain / Inflammation"], dosePerKg:10, maxDoseMg:600, maxDailyMg:2400, frequency:"Every 6–8 hrs", form:"Liquid 100mg/5mL · Tab 200mg · Tab 400mg", note:"Give with food. Avoid < 6 months or if dehydrated/renal concerns." }, { name:"Amoxicillin (standard)", indications:["Otitis Media / Pharyngitis / Skin"], dosePerKg:40, maxDoseMg:500, maxDailyMg:1500, frequency:"Every 8 hrs", form:"Susp 250mg/5mL · Susp 400mg/5mL · Cap 500mg", note:"High-dose: 80–90 mg/kg/day for resistant AOM." }, { name:"Amoxicillin (high-dose)", indications:["Resistant AOM / Sinusitis"], dosePerKg:80, maxDoseMg:875, maxDailyMg:1750, frequency:"Every 12 hrs", form:"Susp 400mg/5mL · Tab 875mg", note:"Use when resistant Strep pneumo suspected." }, { name:"Azithromycin", indications:["Atypical Pneumonia / Strep (PCN allergy)"], dosePerKg:10, maxDoseMg:500, maxDailyMg:500, frequency:"Once daily × 5 days", form:"Susp 200mg/5mL · Tab 250mg", note:"Day 1: 10mg/kg. Days 2–5: 5mg/kg. Max 500mg day 1." }, { name:"Cetirizine", indications:["Allergic Rhinitis / Urticaria"], dosePerKg:0.25, maxDoseMg:10, maxDailyMg:10, frequency:"Once daily", form:"Syrup 5mg/5mL · Tab 5mg / 10mg", note:"Ages 6m–2yr: 2.5mg. Ages 2–5: up to 5mg. Ages ≥6: up to 10mg." }, { name:"Diphenhydramine", indications:["Allergic Rxn / Urticaria / Insomnia"], dosePerKg:1.25, maxDoseMg:50, maxDailyMg:300, frequency:"Every 6 hrs", form:"Liquid 12.5mg/5mL · Cap 25mg · Tab 50mg", note:"Not for use < 2 years. Sedating — use caution." }, ]; // ══════════════════════════════════════════════════════════════════════════════ // DOSING TAB // ══════════════════════════════════════════════════════════════════════════════ function DosingTab() { const [weight, setWeight] = useState(""); const [unit, setUnit] = useState("lbs"); const [selected, setSelected] = useState(null); const weightKg = unit==="lbs" ? lbsToKg(weight) : parseFloat(weight); const weightLbs = unit==="lbs" ? parseFloat(weight) : kgToLbs(weight); const isValid = !isNaN(weightKg) && weightKg>0 && weightKg<=200; const calc = useCallback((med)=>{ if(!isValid) return null; const doseMg = Math.min(med.dosePerKg*weightKg, med.maxDoseMg); const liquidMl = (doseMg/160)*5; const liquidTsp = (liquidMl/4.929).toFixed(2); return { doseMg:doseMg.toFixed(1), liquidMl:liquidMl.toFixed(2), liquidTsp }; },[weightKg, isValid]); return (
setWeight(e.target.value)} style={inputStyle} />
{weight && isValid &&
≈ {weightLbs.toFixed(1)} lbs · {weightKg.toFixed(1)} kg
}
{MEDICATIONS.map(med=>{ const result=calc(med); const open=selected===med.name; return (
{open && (
{result && (
)}
⚠ {med.note}
*Volume based on 160mg/5mL. Verify concentration in hand.
)}
); })}
); } // ══════════════════════════════════════════════════════════════════════════════ // PEDS TAB — all pediatric-specific modules // ══════════════════════════════════════════════════════════════════════════════ const PEDS_MODULES = [ { key:"bili", label:"BiliTool" }, { key:"vitals", label:"Vitals" }, { key:"bp", label:"BP%" }, { key:"apgar", label:"Apgar" }, { key:"ivfluid",label:"IV Fluids" }, { key:"altmed", label:"Antipyretic" }, { key:"growth", label:"Growth Z" }, { key:"dehy", label:"Dehydration" }, ]; function PedsTab() { const [mod, setMod] = useState("bili"); const [vals, setVals] = useState({}); const set = (k,v) => setVals(p=>({...p,[k]:v})); return (
{setMod(k);setVals({});}} /> {mod==="bili" && } {mod==="vitals" && } {mod==="bp" && } {mod==="apgar" && } {mod==="ivfluid" && } {mod==="altmed" && } {mod==="growth" && } {mod==="dehy" && }
); } // ─── BILITOOL ───────────────────────────────────────────────────────────────── // AAP 2022 phototherapy thresholds (mg/dL) by gestational age zone at given hours function getBiliThreshold(ga, hours, risk) { // Simplified AAP 2022 nomogram thresholds (mg/dL) — phototherapy line // risk: "low" = no risk factors, "high" = isoimmune / G6PD / albumin <3 const base = { 24: { low:9.0, high:7.0 }, 36: { low:11.0, high:9.5 }, 48: { low:13.0, high:11.0 }, 60: { low:14.5, high:12.5 }, 72: { low:15.5, high:13.5 }, 84: { low:16.5, high:14.5 }, 96: { low:17.5, high:15.5 }, 120:{ low:18.5, high:16.5 }, 144:{ low:19.0, high:17.0 }, 168:{ low:19.5, high:17.5 }, }; // GA adjustment: reduce threshold by 0.5 for each week below 40 const gaAdj = Math.max(0, (40 - Math.min(parseInt(ga)||40, 40)) * 0.5); const keys = Object.keys(base).map(Number).sort((a,b)=>a-b); let closest = keys[keys.length-1]; for(let k of keys){ if(hours<=k){ closest=k; break; } } const val = base[closest][risk||"low"] - gaAdj; return Math.max(val, 5).toFixed(1); } function BiliTool({ vals, set }) { const ga = parseInt(vals.ga)||0; const hours = parseFloat(vals.hours)||0; const bili = parseFloat(vals.bili)||0; const ready = ga>=35 && hours>=12 && bili>0; let result = null; if(ready){ const lowThresh = parseFloat(getBiliThreshold(ga, hours, "low")); const highThresh = parseFloat(getBiliThreshold(ga, hours, "high")); const exchThresh = lowThresh + 5; if(bili >= exchThresh){ result = { level:"red", risk:"Exchange Transfusion Zone", action:`Bilirubin ${bili} mg/dL meets exchange transfusion threshold (~${exchThresh.toFixed(1)} mg/dL). Urgent neonatology consult.`, thresh:`Photo threshold: ${lowThresh} / ${highThresh} mg/dL` }; } else if(bili >= lowThresh){ result = { level:"yellow", risk:"Phototherapy Recommended", action:`Bilirubin (${bili}) ≥ threshold (${lowThresh} mg/dL at ${hours}h for ${ga}w GA). Start phototherapy. Recheck in 4–6 hrs.`, thresh:`High-risk threshold: ${highThresh} mg/dL` }; } else { result = { level:"green", risk:"Below Phototherapy Threshold", action:`Bilirubin (${bili}) < ${lowThresh} mg/dL (threshold at ${hours}h, ${ga}w GA). Supportive care. Recheck per clinical judgment.`, thresh:`High-risk threshold: ${highThresh} mg/dL` }; } } return (
{[["low","None / Low"],["high","High (isoimmune, G6PD, albumin <3)"]].map(([v,l])=>( ))}
{result && ( <>
{result.thresh}
)}
Based on AAP 2022 Clinical Practice Guideline. Confirm with institution protocol.
); } // ─── VITALS NORMS ───────────────────────────────────────────────────────────── const VITALS_DATA = [ { age:"Newborn (0–1 mo)", rr:"40–60", hr:"100–160", spo2:"≥95%", sbp:"60–90", concern:"RR>60 or <30, SpO₂<94%" }, { age:"Infant (1–12 mo)", rr:"30–50", hr:"100–160", spo2:"≥96%", sbp:"70–100", concern:"RR>50, SpO₂<95%" }, { age:"Toddler (1–3 yr)", rr:"24–40", hr:"90–150", spo2:"≥97%", sbp:"80–110", concern:"RR>40, SpO₂<95%" }, { age:"Preschool (3–6 yr)",rr:"22–34", hr:"80–140", spo2:"≥97%", sbp:"80–110", concern:"RR>34, SpO₂<95%" }, { age:"School (6–12 yr)", rr:"18–30", hr:"70–120", spo2:"≥97%", sbp:"85–120", concern:"RR>30, SpO₂<95%" }, { age:"Adolescent (>12)", rr:"12–20", hr:"60–100", spo2:"≥97%", sbp:"90–130", concern:"RR>20, SpO₂<95%" }, ]; function VitalsNorms() { return (
Respiratory Rate & SpO₂ Age Norms
Breaths/min · HR (bpm) · SpO₂ · SBP (mmHg)
{VITALS_DATA.map(v=>(
{v.age}
⚠ {v.concern}
))}
); } // ─── BP PERCENTILE ──────────────────────────────────────────────────────────── // Simplified AAP 2017 BP classification by age/sex/height percentile // Returns 50th/90th/95th percentile SBP approximations function getBPNorms(age, sex, htPct) { // Base 50th percentile SBP from AAP 2017 table approximations const maleSBP50 = { 1:85,2:87,3:88,4:90,5:91,6:92,7:92,8:93,9:94,10:95,11:96,12:97,13:98 }; const femaleSBP50 = { 1:83,2:85,3:86,4:88,5:89,6:90,7:91,8:91,9:92,10:93,11:94,12:95,13:96 }; const base = (sex==="male"?maleSBP50:femaleSBP50)[Math.min(Math.max(parseInt(age)||1,1),13)] || 90; const htAdj = htPct==="25"?-1:htPct==="75"?1:htPct==="90"?2:0; const p50 = base + htAdj; const p90 = p50 + 11; const p95 = p50 + 13; const p99 = p50 + 18; return { p50, p90, p95, p99 }; } function BPPercentile({ vals, set }) { const age = parseInt(vals.bpage)||0; const sbp = parseInt(vals.sbp)||0; const dbp = parseInt(vals.dbp)||0; const ready = age>=1 && age<=17 && sbp>0; let result = null; if(ready && age<=13){ const norms = getBPNorms(age, vals.bpsex||"male", vals.htpct||"50"); if(sbp >= norms.p99 || dbp >= 80){ result = { level:"red", risk:"Stage 2 Hypertension", action:`SBP ≥ 99th percentile (${norms.p99} mmHg) or DBP ≥ 80. Evaluate for secondary causes. Prompt referral.` }; } else if(sbp >= norms.p95){ result = { level:"red", risk:"Stage 1 Hypertension", action:`SBP ≥ 95th percentile (${norms.p95} mmHg). Confirm on 3 occasions. Lifestyle + follow-up.` }; } else if(sbp >= norms.p90){ result = { level:"yellow", risk:"Elevated BP", action:`SBP ≥ 90th percentile (${norms.p90} mmHg). Recheck in 6 months. Lifestyle counseling.` }; } else { result = { level:"green", risk:"Normal BP", action:`SBP below 90th percentile (${norms.p90} mmHg) for age/sex/height.` }; } result.norms = norms; } else if(ready && age>=13){ // Adolescent ≥ 13: use adult thresholds if(sbp>=130||dbp>=80) result={level:"red",risk:"Stage 1 HTN (adolescent)",action:"SBP ≥ 130 or DBP ≥ 80. Lifestyle modification. Evaluate secondary causes."}; else if(sbp>=120||dbp>=80) result={level:"yellow",risk:"Elevated BP (adolescent)",action:"SBP 120–129 and DBP <80. Lifestyle counseling. Recheck."}; else result={level:"green",risk:"Normal BP (adolescent)",action:"Below elevated threshold for adolescent."}; } return ( {parseInt(vals.bpage||0)<13 && (
)} {result && ( <> {result.norms && (
)} )}
); } // ─── APGAR SCORE ────────────────────────────────────────────────────────────── const APGAR_ITEMS = [ { id:"appear", label:"Appearance (Skin Color)", options:[{v:0,l:"Blue/pale all over"},{v:1,l:"Blue hands/feet, pink body"},{v:2,l:"Pink all over"}] }, { id:"pulse", label:"Pulse (Heart Rate)", options:[{v:0,l:"Absent"},{v:1,l:"< 100 bpm"},{v:2,l:"≥ 100 bpm"}] }, { id:"grimace",label:"Grimace (Reflex)", options:[{v:0,l:"No response"},{v:1,l:"Grimace only"},{v:2,l:"Cry, cough, sneeze"}] }, { id:"active", label:"Activity (Muscle Tone)", options:[{v:0,l:"Limp"},{v:1,l:"Some flexion"},{v:2,l:"Active motion"}] }, { id:"resp", label:"Respiration", options:[{v:0,l:"Absent"},{v:1,l:"Weak / irregular"},{v:2,l:"Strong cry"}] }, ]; function ApgarScore({ vals, set }) { const [minute, setMinute] = useState("1"); const key = m => `apgar_${m}_`; const score = APGAR_ITEMS.reduce((s,item)=>{ const v = vals[key(minute)+item.id]; return s + (v!==undefined ? parseInt(v) : 0); }, 0); const allSet = APGAR_ITEMS.every(item=>vals[key(minute)+item.id]!==undefined); let result = null; if(allSet){ if(score>=7) result={level:"green",risk:`Score ${score} — Normal`,action:"Continue routine newborn care. Reassess at 5 minutes."}; else if(score>=4) result={level:"yellow",risk:`Score ${score} — Moderate concern`,action:"Stimulate, suction, supplemental O₂. Reassess at 5 min. Consider NICU consult."}; else result={level:"red",risk:`Score ${score} — Severe / Requires resuscitation`,action:"Immediate resuscitation. Warm, dry, stimulate, PPV if needed. NICU."}; } return (
{["1","5","10"].map(m=>( ))}
{APGAR_ITEMS.map(item=>(
{item.options.map(opt=>{ const k = key(minute)+item.id; const sel = vals[k]===String(opt.v)||vals[k]===opt.v; return ( ); })}
))}
{minute}-min Score {allSet?score:"—"}/10
{result && }
); } // ─── IV FLUID (HOLLIDAY-SEGAR 4-2-1) ───────────────────────────────────────── function calcIVFluid(weightLbs) { const wt = lbsToKg(weightLbs); let mlPerHr = 0; if(wt<=10) mlPerHr = wt*4; else if(wt<=20) mlPerHr = 40+(wt-10)*2; else mlPerHr = 60+(wt-20)*1; const daily = mlPerHr*24; const dailyOz = (daily/29.574).toFixed(1); return { mlPerHr:mlPerHr.toFixed(1), daily:Math.round(daily), dailyOz, wt:wt.toFixed(1) }; } function IVFluid({ vals, set }) { const ready = vals.ivwt && parseFloat(vals.ivwt)>0; const result = ready ? calcIVFluid(parseFloat(vals.ivwt)) : null; return ( {result && (
)}
Rule: 4 mL/kg/hr for first 10kg · 2 mL/kg/hr for next 10kg · 1 mL/kg/hr for each kg after 20kg. Typical fluid: D5 ½NS + 20mEq/L KCl.
); } // ─── ANTIPYRETIC ALTERNATING SCHEDULE ───────────────────────────────────────── function calcAltMed(weightLbs) { const wt = lbsToKg(weightLbs); const apapMg = Math.min(15*wt, 1000); const ibupMg = Math.min(10*wt, 600); const apapTsp = ((apapMg/160)*5/4.929).toFixed(2); const ibupTsp = ((ibupMg/100)*5/4.929).toFixed(2); return { apapMg:apapMg.toFixed(1), ibupMg:ibupMg.toFixed(1), apapTsp, ibupTsp }; } function AltMed({ vals, set }) { const ready = vals.altwt && parseFloat(vals.altwt)>0 && lbsToKg(parseFloat(vals.altwt))>=6; const result = ready ? calcAltMed(parseFloat(vals.altwt)) : null; const schedule = result ? [ { time:"0 hrs", drug:"Ibuprofen", dose:`${result.ibupMg} mg (${result.ibupTsp} tsp)`, color:COLORS.blue }, { time:"3 hrs", drug:"Acetaminophen",dose:`${result.apapMg} mg (${result.apapTsp} tsp)`, color:COLORS.teal }, { time:"6 hrs", drug:"Ibuprofen", dose:`${result.ibupMg} mg (${result.ibupTsp} tsp)`, color:COLORS.blue }, { time:"9 hrs", drug:"Acetaminophen",dose:`${result.apapMg} mg (${result.apapTsp} tsp)`, color:COLORS.teal }, { time:"12 hrs", drug:"Ibuprofen", dose:`${result.ibupMg} mg (${result.ibupTsp} tsp)`, color:COLORS.blue }, ] : []; return ( {ready && result && ( <>
Sample 12-hour Schedule
{schedule.map(s=>(
{s.time}
{s.drug}
{s.dose}
))}
⚠ Ibuprofen not for <6 months or <12 lbs. Do not exceed labeled max daily doses. Give with food.
)}
); } // ─── GROWTH Z-SCORE / WEIGHT-FOR-AGE ───────────────────────────────────────── // WHO/CDC simplified weight-for-age median and SD values (approximate, 0–18 yrs) const WEIGHT_NORMS = { male: [ {age:0, p3:2.5, p5:2.8, p10:3.0, p25:3.3, p50:3.5, p75:3.8, p85:4.0, p95:4.3, p97:4.5}, {age:6, p3:5.7, p5:6.1, p10:6.5, p25:7.1, p50:7.9, p75:8.7, p85:9.1, p95:9.9, p97:10.2}, {age:12, p3:7.7, p5:8.1, p10:8.7, p25:9.5, p50:10.2,p75:11.3,p85:11.8,p95:12.9,p97:13.5}, {age:24, p3:10.5, p5:11.0, p10:11.5,p25:12.4,p50:13.5,p75:14.7,p85:15.4,p95:16.8,p97:17.4}, {age:36, p3:12.5, p5:13.0, p10:13.7,p25:14.7,p50:16.0,p75:17.5,p85:18.4,p95:20.1,p97:21.0}, {age:60, p3:15.3, p5:16.0, p10:16.9,p25:18.3,p50:20.0,p75:22.3,p85:23.6,p95:26.3,p97:27.8}, {age:120,p3:24.7, p5:26.0, p10:27.7,p25:30.8,p50:34.7,p75:39.9,p85:43.5,p95:50.7,p97:54.5}, {age:180,p3:46.0, p5:49.0, p10:52.5,p25:58.5,p50:65.0,p75:74.0,p85:80.0,p95:92.0,p97:97.0}, ], female: [ {age:0, p3:2.4, p5:2.7, p10:2.9, p25:3.2, p50:3.4, p75:3.7, p85:3.9, p95:4.2, p97:4.4}, {age:6, p3:5.4, p5:5.7, p10:6.1, p25:6.7, p50:7.3, p75:8.0, p85:8.5, p95:9.3, p97:9.6}, {age:12, p3:7.1, p5:7.5, p10:8.0, p25:8.8, p50:9.5, p75:10.5,p85:11.0,p95:12.0,p97:12.5}, {age:24, p3:10.0, p5:10.5, p10:11.1,p25:12.0,p50:13.1,p75:14.4,p85:15.2,p95:16.7,p97:17.5}, {age:36, p3:12.0, p5:12.5, p10:13.2,p25:14.3,p50:15.6,p75:17.3,p85:18.3,p95:20.3,p97:21.4}, {age:60, p3:14.7, p5:15.4, p10:16.3,p25:17.8,p50:19.5,p75:22.0,p85:23.6,p95:26.8,p97:28.6}, {age:120,p3:23.5, p5:24.8, p10:26.5,p25:29.7,p50:33.7,p75:39.5,p85:43.4,p95:52.0,p97:56.6}, {age:180,p3:42.0, p5:45.0, p10:49.0,p25:55.0,p50:62.0,p75:72.0,p85:79.0,p95:93.0,p97:100.0}, ] }; function getWeightPercentile(ageMo, weightKg, sex) { const table = WEIGHT_NORMS[sex]||WEIGHT_NORMS.male; let row = table[0]; for(let i=1;i=table[i].age) row=table[i]; } const p50=row.p50; if(weightKg<=row.p3) return {pct:"< 3rd", level:"red", ftt:true}; if(weightKg<=row.p5) return {pct:"3rd–5th", level:"red", ftt:true}; if(weightKg<=row.p10) return {pct:"5th–10th", level:"yellow", ftt:true}; if(weightKg<=row.p25) return {pct:"10th–25th", level:"yellow", ftt:false}; if(weightKg<=row.p75) return {pct:"25th–75th", level:"green", ftt:false}; if(weightKg<=row.p85) return {pct:"75th–85th", level:"green", ftt:false}; if(weightKg<=row.p95) return {pct:"85th–95th", level:"yellow", ftt:false}; return {pct:"> 95th", level:"red", ftt:false, obese:true}; } function GrowthZ({ vals, set }) { const ageMo = parseFloat(vals.gage)||0; const weightLbs = parseFloat(vals.gwt)||0; const weightKg = lbsToKg(weightLbs); const ready = ageMo>0 && weightLbs>0 && ageMo<=216; let result=null; if(ready){ const r = getWeightPercentile(ageMo, weightKg, vals.gsex||"male"); const fttText = r.ftt ? "⚠ Weight-for-age below 10th percentile. Evaluate for Failure to Thrive (FTT). Consider dietary history, growth velocity, and referral." : ""; const obText = r.obese ? "⚠ Weight-for-age > 95th percentile. Obesity risk. Evaluate diet, activity, family history." : ""; result = { ...r, action: fttText||obText||"Weight within expected range for age. Continue routine monitoring.", risk:`${r.pct} percentile` }; } return ( {ready && result && ( <>
)}
); } // ─── DEHYDRATION SEVERITY (GORELICK) ───────────────────────────────────────── const GORELICK_ITEMS = [ { id:"caprefill", label:"Capillary refill time", options:[{v:0,l:"< 2 seconds (normal)"},{v:1,l:"2–3 seconds"},{v:2,l:"> 3 seconds"}] }, { id:"turgor", label:"Skin turgor", options:[{v:0,l:"Normal (immediate recoil)"},{v:1,l:"Reduced (slow recoil)"}] }, { id:"mucous", label:"Mucous membranes", options:[{v:0,l:"Moist"},{v:1,l:"Dry/sticky"}] }, { id:"eyes", label:"Eyes", options:[{v:0,l:"Normal"},{v:1,l:"Sunken"}] }, { id:"tears", label:"Tears", options:[{v:0,l:"Present"},{v:1,l:"Decreased/absent"}] }, { id:"general", label:"General appearance", options:[{v:0,l:"Normal"},{v:1,l:"Ill-appearing"}] }, { id:"hr", label:"Heart rate", options:[{v:0,l:"Normal for age"},{v:1,l:"Mildly elevated"},{v:2,l:"Markedly elevated"}] }, { id:"breathing", label:"Breathing pattern", options:[{v:0,l:"Normal"},{v:1,l:"Deep/rapid"}] }, { id:"bp", label:"Blood pressure", options:[{v:0,l:"Normal"},{v:1,l:"Hypotensive/orthostatic"}] }, { id:"urine", label:"Urine output", options:[{v:0,l:"Normal"},{v:1,l:"Decreased"}] }, ]; function Dehydration({ vals, set }) { const score = GORELICK_ITEMS.reduce((s,item)=>{ const v=vals["deh_"+item.id]; return s+(v!==undefined?parseInt(v):0); },0); const allSet = GORELICK_ITEMS.every(item=>vals["deh_"+item.id]!==undefined); let result=null; if(allSet){ if(score<=1) result={level:"green",risk:"Minimal / No Dehydration",action:"< 3% fluid deficit. Encourage oral fluids. Continue breastfeeding/formula. No ORS required."}; else if(score<=5) result={level:"yellow",risk:"Mild–Moderate Dehydration (3–9%)",action:"ORS 50–100 mL/kg over 2–4 hrs. Small frequent sips. Replace ongoing losses (10 mL/kg per vomit/stool). Reassess frequently."}; else result={level:"red",risk:"Severe Dehydration (> 9%)",action:"IV fluid resuscitation. NS 20 mL/kg bolus; repeat as needed. Hospital admission. Monitor electrolytes."}; } return ( {GORELICK_ITEMS.map(item=>(
{item.options.map(opt=>{ const k="deh_"+item.id; const sel=vals[k]===String(opt.v)||vals[k]===opt.v; return ( ); })}
))}
Severity Score {allSet?score:"—"}
{result && }
); } // ══════════════════════════════════════════════════════════════════════════════ // SCORING TAB (unchanged from before) // ══════════════════════════════════════════════════════════════════════════════ const SCORES = { centor: { name:"Centor / McIsaac Score", subtitle:"Strep Pharyngitis Risk", description:"Estimates probability of Group A Strep in pharyngitis.", criteria:[ {id:"exudate",label:"Tonsillar exudate or swelling",points:1}, {id:"nodes",label:"Tender anterior cervical lymphadenopathy",points:1}, {id:"fever",label:"Fever history (> 100.4°F)",points:1}, {id:"nocough",label:"Absence of cough",points:1}, {id:"age3_14",label:"Age 3–14 years",points:1}, {id:"age15_44",label:"Age 15–44 years",points:0}, {id:"age45plus",label:"Age ≥ 45 years",points:-1}, ], ageOptions:["age3_14","age15_44","age45plus"], interpret:(score)=>{ if(score<=0) return {risk:"Low",action:"No testing or antibiotics needed.",level:"green"}; if(score===1) return {risk:"Low (5–10%)",action:"No testing or antibiotics recommended.",level:"green"}; if(score===2) return {risk:"Moderate (11–17%)",action:"Consider rapid strep test. Treat if positive.",level:"yellow"}; if(score===3) return {risk:"Moderate-High (28–35%)",action:"Rapid strep test recommended. Treat if positive.",level:"yellow"}; return {risk:"High (51–53%)",action:"Consider empiric antibiotics or confirm with rapid test.",level:"red"}; }, }, westley: { name:"Westley Croup Score", subtitle:"Croup Severity", description:"Assesses severity of croup (laryngotracheobronchitis).", criteria:[ {id:"stridor0",label:"Stridor: None",points:0},{id:"stridor1",label:"Stridor: With agitation",points:1},{id:"stridor2",label:"Stridor: At rest",points:2}, {id:"retractions0",label:"Retractions: None",points:0},{id:"retractions1",label:"Retractions: Mild",points:1},{id:"retractions2",label:"Retractions: Moderate",points:2},{id:"retractions3",label:"Retractions: Severe",points:3}, {id:"airentry0",label:"Air Entry: Normal",points:0},{id:"airentry1",label:"Air Entry: Decreased",points:1},{id:"airentry2",label:"Air Entry: Markedly decreased",points:2}, {id:"cyan0",label:"Cyanosis: None",points:0},{id:"cyan4",label:"Cyanosis: With agitation",points:4},{id:"cyan5",label:"Cyanosis: At rest",points:5}, {id:"consc0",label:"Consciousness: Normal",points:0},{id:"consc5",label:"Consciousness: Altered",points:5}, ], groups:[ {label:"Stridor",ids:["stridor0","stridor1","stridor2"]}, {label:"Retractions",ids:["retractions0","retractions1","retractions2","retractions3"]}, {label:"Air Entry",ids:["airentry0","airentry1","airentry2"]}, {label:"Cyanosis",ids:["cyan0","cyan4","cyan5"]}, {label:"Level of Consciousness",ids:["consc0","consc5"]}, ], interpret:(score)=>{ if(score<=2) return {risk:"Mild",action:"Outpatient care. Dexamethasone 0.15mg/kg PO. Supportive care.",level:"green"}; if(score<=5) return {risk:"Moderate",action:"Dexamethasone 0.6mg/kg IM/PO. Consider nebulized epinephrine. Monitor 2–4 hrs.",level:"yellow"}; if(score<=11) return {risk:"Severe",action:"Nebulized epinephrine + Dexamethasone. Hospital admission likely.",level:"red"}; return {risk:"Impending respiratory failure",action:"Immediate airway management. ICU admission.",level:"red"}; }, }, pecarn: { name:"PECARN Head Injury", subtitle:"CT Head Decision Rule (< 2 years)", description:"Identifies low-risk children < 2 years old after head trauma.", criteria:[ {id:"gcs14",label:"GCS < 15",points:2}, {id:"ams",label:"Altered mental status (agitation, somnolence, repetitive questions, slow response)",points:2}, {id:"palp_skull",label:"Palpable skull fracture",points:2}, {id:"occipital",label:"Occipital / parietal / temporal scalp hematoma",points:1}, {id:"loc",label:"Loss of consciousness ≥ 5 seconds",points:1}, {id:"severe_mech",label:"Severe injury mechanism (MVA, fall > 3 ft, struck by high-impact object)",points:1}, {id:"not_acting",label:"Not acting normally per parent",points:1}, ], interpret:(score)=>{ if(score>=4) return {risk:"High",action:"CT head recommended.",level:"red"}; if(score>=1) return {risk:"Intermediate",action:"CT vs. observation based on clinical judgment. Discuss with caregiver.",level:"yellow"}; return {risk:"Low (< 0.02% ciTBI)",action:"CT not required. Observation acceptable. Discharge with return precautions.",level:"green"}; }, }, }; function ScoringTab() { const [activeScore, setActiveScore] = useState("centor"); const [checked, setChecked] = useState({}); const score = SCORES[activeScore]; const ageOptions = score.ageOptions||[]; const toggle=(id)=>{ if(ageOptions.includes(id)){ setChecked(prev=>{ const next={...prev}; ageOptions.forEach(a=>delete next[a]); if(!prev[id]) next[id]=true; return next; }); } else { setChecked(prev=>({...prev,[id]:!prev[id]})); } }; const total = score.criteria.reduce((s,c)=>checked[c.id]?s+c.points:s,0); const result = score.interpret(total); const renderCriteria=()=>{ if(score.groups) return score.groups.map(g=>(
{g.label}
{g.ids.map(id=>{ const c=score.criteria.find(x=>x.id===id); return toggle(id)} radio={true} />; })}
)); return score.criteria.map(c=>toggle(c.id)} radio={ageOptions.includes(c.id)} />); }; return (
{Object.entries(SCORES).map(([key,s])=>( ))}
{score.name}
{score.description}
{renderCriteria()}
Total Score {total}
); } function CheckRow({ criterion, checked, onChange, radio }) { return ( ); } // ══════════════════════════════════════════════════════════════════════════════ // GENERAL TAB // ══════════════════════════════════════════════════════════════════════════════ const GENERAL_MODULES = [ {key:"bmi", label:"BMI"}, {key:"bsa", label:"BSA"}, {key:"ibw", label:"IBW"}, {key:"crcl", label:"CrCl"}, {key:"fluid", label:"Fluids"}, ]; function calcBMI(weightLbs, htFt, htIn) { const totalInches = parseFloat(htFt)*12+parseFloat(htIn||0); return ((weightLbs/(totalInches*totalInches))*703).toFixed(1); } function bmiCategory(bmi) { if(bmi<18.5) return {label:"Underweight",level:"yellow"}; if(bmi<25) return {label:"Normal",level:"green"}; if(bmi<30) return {label:"Overweight",level:"yellow"}; return {label:"Obese",level:"red"}; } function calcBSA(weightLbs, htFt, htIn) { const wt=lbsToKg(weightLbs); const ht=feetInchesToCm(htFt,htIn); return Math.sqrt((wt*ht)/3600).toFixed(2); } function calcIBW(htFt, htIn, sex) { const totalIn=parseFloat(htFt||0)*12+parseFloat(htIn||0); const base=sex==="male"?50:45.5; const ibwKg=Math.max(base+2.3*(totalIn-60),0); return {kg:ibwKg.toFixed(1), lbs:kgToLbs(ibwKg).toFixed(1)}; } function calcCrCl(age, weightLbs, creatinine, sex) { const wt=lbsToKg(weightLbs); const base=((140-age)*wt)/(72*creatinine); return sex==="female"?(base*0.85).toFixed(1):base.toFixed(1); } function calcMaintFluid(weightLbs) { const wt=lbsToKg(weightLbs); let ml=0; if(wt<=10) ml=wt*100; else if(wt<=20) ml=1000+(wt-10)*50; else ml=1500+(wt-20)*20; return {ml:Math.round(ml), oz:(ml/29.574).toFixed(1), rate:(ml/24).toFixed(1)}; } function GeneralTab() { const [active, setActive] = useState("bmi"); const [vals, setVals] = useState({}); const set=(k,v)=>setVals(p=>({...p,[k]:v})); const renderCalc=()=>{ switch(active){ case "bmi": { const bmi=vals.wt&&vals.htft?calcBMI(parseFloat(vals.wt),vals.htft,vals.htin||0):null; const cat=bmi?bmiCategory(parseFloat(bmi)):null; return {bmi&&}; } case "bsa": { const bsa=vals.wt2&&vals.htft2?calcBSA(parseFloat(vals.wt2),vals.htft2,vals.htin2||0):null; return {bsa&&}; } case "ibw": { const ibw=vals.htft3?calcIBW(vals.htft3,vals.htin3||0,vals.sex||"male"):null; return {ibw&&
}
; } case "crcl": { const crcl=vals.age4&&vals.wt4&&vals.cr?calcCrCl(parseFloat(vals.age4),parseFloat(vals.wt4),parseFloat(vals.cr),vals.sex4||"male"):null; return {crcl&&}; } case "fluid": { const result=vals.wt5?calcMaintFluid(parseFloat(vals.wt5)):null; return {result&&
}
; } default: return null; } }; return (
{setActive(k);setVals({});}} /> {renderCalc()}
); } // ══════════════════════════════════════════════════════════════════════════════ // APP ROOT // ══════════════════════════════════════════════════════════════════════════════ export default function App() { const [tab, setTab] = useState("Dosing"); return (
ClinCalc
Point-of-Care Medical Calculator
{TABS.map(t=>( ))}
{tab==="Dosing" && } {tab==="Peds" && } {tab==="Scoring" && } {tab==="General" && }

For clinical decision support only. Always verify dosing against current references and use clinical judgment. Not a substitute for professional medical advice.

); }