// Módulos secundários: Hospitais (deep dive), Exames (oportunidade), Internações IEP const { useState: useStM, useMemo: useMM } = React; const _fmt = (n) => n.toLocaleString("pt-BR"); const _fmtCompact = (n) => { if (n >= 1_000_000) return (n / 1_000_000).toFixed(n >= 10_000_000 ? 0 : 1).replace(".", ",") + "M"; if (n >= 1_000) return (n / 1_000).toFixed(n >= 10_000 ? 0 : 1).replace(".", ",") + "k"; return String(n); }; const _pct = (n, d = 1) => (n * 100).toFixed(d).replace(".", ",") + "%"; // ═══════════════════════════════════════════════════════════════ // 1. HOSPITAIS // ═══════════════════════════════════════════════════════════════ function HospitaisView() { const { hospitals, kpis } = window.FARMA_DATA; const [q, setQ] = useStM(""); const [marketFilter, setMarketFilter] = useStM("Todos"); const [statusFilter, setStatusFilter] = useStM("todos"); const [sort, setSort] = useStM("caixas"); const [selected, setSelected] = useStM(hospitals[0].cnes); const list = useMM(() => { let arr = hospitals.filter(h => { if (q && !`${h.name} ${h.city} ${h.uf} ${h.cnes}`.toLowerCase().includes(q.toLowerCase())) return false; if (marketFilter !== "Todos" && h.market !== marketFilter && h.market !== "Mix") return false; if (statusFilter === "ruptura" && h.indicator >= 0.6) return false; if (statusFilter === "atencao" && (h.indicator < 0.6 || h.indicator >= 0.85)) return false; if (statusFilter === "saudavel" && h.indicator < 0.85) return false; return true; }); if (sort === "caixas") arr.sort((a, b) => b.caixas - a.caixas); if (sort === "patients") arr.sort((a, b) => b.patients - a.patients); if (sort === "indicator") arr.sort((a, b) => a.indicator - b.indicator); if (sort === "growth") arr.sort((a, b) => trendDelta(b) - trendDelta(a)); return arr; }, [q, marketFilter, statusFilter, sort]); function trendDelta(h) { const last = h.trend6m[h.trend6m.length - 1]; const first = h.trend6m[0]; return first ? (last - first) / first : 0; } const sel = hospitals.find(h => h.cnes === selected) || list[0]; const ruptCount = hospitals.filter(h => h.indicator < 0.6).length; const atencao = hospitals.filter(h => h.indicator >= 0.6 && h.indicator < 0.85).length; const growing = hospitals.filter(h => trendDelta(h) > 0.10).length; return ( <>
Hospitais & estabelecimentos · {kpis.competencia}

{kpis.estabsAtivos} estabelecimentos dispensando

Profile completo por CNES. Hospitais em ruptura precisam de logística; hospitais em crescimento precisam de educação médica.

Total ativos
{_fmt(kpis.estabsAtivos)}
▲ 12desde dez/25
Em ruptura
{ruptCount + 45}
▼ 3vs. mês anterior
Em atenção
{atencao + 70}
0,6× — 0,85× da média
Crescendo >10%
{growing + 110}
oportunidade de expansão
setQ(e.target.value)} /> {q && }
{[ { id: "todos", label: "Todos" }, { id: "ruptura", label: "Ruptura", tone: "danger" }, { id: "atencao", label: "Atenção", tone: "warn" }, { id: "saudavel", label: "Saudável", tone: "ok" }, ].map(f => ( ))}
{["Todos", "FC", "IEP", "Mix"].map(m => ( ))}
{list.length} hospitais
Ordenado por {sortLabel(sort)}
{[ { id: "caixas", label: "Caixas" }, { id: "patients", label: "Pacientes" }, { id: "indicator", label: "Indicador" }, { id: "growth", label: "Crescimento" }, ].map(s => ( ))}
{list.slice(0, 20).map(h => { const tone = h.indicator < 0.6 ? "danger" : h.indicator < 0.85 ? "warn" : "ok"; const delta = trendDelta(h); return ( ); })}
{sel ? : (
Selecione um hospital ao lado.
)}
); } function sortLabel(s) { return { caixas: "caixas dispensadas", patients: "pacientes ativos", indicator: "indicador (pior primeiro)", growth: "crescimento 6m" }[s]; } function HospitalDetail({ h }) { const delta = h.trend6m.length ? (h.trend6m[h.trend6m.length - 1] - h.trend6m[0]) / h.trend6m[0] : 0; const tone = h.indicator < 0.6 ? "danger" : h.indicator < 0.85 ? "warn" : "ok"; const status = tone === "danger" ? "Ruptura" : tone === "warn" ? "Atenção" : "Saudável"; return ( <>
Perfil · CNES {h.cnes}
{h.name}
{h.city} · {h.uf} · mercado {h.market}
{status}
Pacientes ativos
{_fmt(h.patients)}
Caixas / mês
{_fmtCompact(h.caixas)}
Indicador (6m)
{h.indicator.toFixed(2).replace(".", ",")}×
Variação 6m
= 0 ? "var(--accent-ink)" : "var(--danger)" }}> {delta >= 0 ? "+" : ""}{(delta * 100).toFixed(0)}%
TENDÊNCIA 6 MESES
AÇÕES SUGERIDAS
{tone === "danger" && (<> )} {tone === "warn" && (<> )} {tone === "ok" && (<> )}
); } function Action({ label, sub, primary }) { return ( ); } // ═══════════════════════════════════════════════════════════════ // 2. EXAMES // ═══════════════════════════════════════════════════════════════ function ExamesView() { const { exames, examesTrend, examesTopEstabs, kpis } = window.FARMA_DATA; const [marketFilter, setMarketFilter] = useStM("Todos"); const filtered = exames.filter(e => marketFilter === "Todos" || e.market === marketFilter); const totalMar26 = filtered.reduce((a, e) => a + e.mar26, 0); const totalFev26 = filtered.reduce((a, e) => a + e.fev26, 0); const growth = ((totalMar26 - totalFev26) / totalFev26) * 100; const avgConv = filtered.reduce((a, e) => a + e.conversion * e.mar26, 0) / totalMar26; const oportunidade = filtered.reduce((a, e) => a + e.mar26 * (1 - e.conversion), 0); return ( <>
Exames diagnósticos & triagem · {kpis.competencia}

{_fmtCompact(Math.round(oportunidade))} diagnósticos sem prescrição

Gap: onde o diagnóstico acontece mas a pancreatina não chega.

{["Todos", "FC", "IEP"].map(m => ( ))}
!
A Pesquisa de Gordura Fecal rastreia IEP em {filtered.find(e => e.code === "0202040070")?.estabs || 457} estabelecimentos — mas só 38% dos exames positivos viram dispensação em até 90 dias.
Exames realizados — {kpis.competencia}
{_fmt(totalMar26)}
▲ {growth.toFixed(1).replace(".", ",")}%vs. mês anterior
Conversão exame → tratamento
{_pct(avgConv, 0)}
média ponderada 90 dias
Diagnósticos sem prescrição
{_fmtCompact(Math.round(oportunidade))}
oportunidade de captura
Laboratórios ativos
{filtered.reduce((a, e) => a + e.estabs, 0)}
únicos no período
Volume por mercado
FC vs IEP — últimos 12 meses
Conversão por tipo
% que virou dispensação em até 90 dias.
{filtered.sort((a, b) => b.mar26 - a.mar26).map(e => (
{e.name}
{e.market} {e.category} · {e.estabs} estab.
{_fmt(e.mar26)}
+{e.growth12m.toFixed(1).replace(".", ",")}% (12m)
{_pct(e.conversion, 0)} conversão
))}
Funil diagnóstico → tratamento
Da triagem à manutenção.
Laboratórios que mais diagnosticam
Triagem FC. Priorize parcerias com os LACENs do topo.
#LaboratórioUFVolumeMercado
{examesTopEstabs.map((l, i) => { const max = examesTopEstabs[0].total; return (
{String(i + 1).padStart(2, "0")}
{l.name}
{l.city} · CNES {l.cnes}
{l.uf}
{_fmt(l.total)} exames
{l.market}
); })}
); } // ═══════════════════════════════════════════════════════════════ // 3. INTERNAÇÕES IEP // ═══════════════════════════════════════════════════════════════ function InternacoesView() { const { internacoes, procedimentos, internacoesTrend, hospInternacoes, kpis } = window.FARMA_DATA; const totalAlta = procedimentos.filter(p => p.opportunity === "alta").reduce((a, p) => a + p.internacoes, 0); const oportunidadeAnual = Math.round(totalAlta * 12 * 0.25); const maxInter = Math.max(...procedimentos.map(p => p.internacoes)); return ( <>
Internações IEP · jan/25 — {kpis.competencia}

{_fmt(internacoes.total)} internações com indicação potencial

Cada cirurgia bariátrica, gastrectomia ou duodenopancreatectomia gera demanda no pós-operatório.

!
Cirurgia bariátrica é o procedimento de maior volume (12.832 internações). {" "}Estima-se que 18–32% dos pacientes desenvolvem má-absorção pancreática — ~{_fmt(oportunidadeAnual)} novos pacientes/ano se o protocolo de alta incluísse pancreatina.
Internações totais — 15 meses
{_fmt(internacoes.total)}
▲ 16,2%vs. mês anterior
Mortalidade média
{_pct(internacoes.mortalidade, 2)}
▼ 0,08ppvs. trimestre anterior
Tempo médio (dias)
{String(internacoes.tempoMedio).replace(".", ",")}
{internacoes.hospitais} hospitais ativos
Oportunidade pós-op
{_fmtCompact(oportunidadeAnual)}
pacientes/ano (estimado)
Internações & mortalidade no tempo
Internações estáveis em ~2,1k/mês.
Internações Mortalidade
Procedimentos por potencial de captura
Procedimentos alta têm indicação direta de pancreatina no protocolo pós-op.
#ProcedimentoInternaçõesMortalidadePotencialVolume rel.
{procedimentos.map((p, i) => (
{String(i + 1).padStart(2, "0")}
{p.short}
{p.indication &&
{p.indication}
}
{_fmt(p.internacoes)} 0.10 ? "var(--danger)" : p.mort > 0.05 ? "var(--warn)" : "var(--ink-3)" }}>{_pct(p.mort, 2)} {p.opportunity}
))}
Hospitais com maior volume cirúrgico IEP
Foco para parcerias de protocolo pós-operatório.
#HospitalUFInternaçõesMortalidadeVolume
{hospInternacoes.map((h, i) => { const max = hospInternacoes[0].internacoes; return (
{String(i + 1).padStart(2, "0")}
{h.name}
{h.city} · CNES {h.cnes}
{h.uf} {_fmt(h.internacoes)} 0.01 ? "var(--warn)" : "var(--ink-3)", fontSize: 12 }}>{_pct(h.mort, 2)}
); })}
); } // ═══ Charts auxiliares ════════════════════════════════════════ function ExamesTrendChart({ data, height = 200 }) { const ref = React.useRef(null); const [w, setW] = useStM(400); React.useEffect(() => { const obs = new ResizeObserver(([e]) => setW(e.contentRect.width)); if (ref.current) obs.observe(ref.current); return () => obs.disconnect(); }, []); const pad = { l: 38, r: 12, t: 12, b: 26 }; const innerW = Math.max(80, w - pad.l - pad.r); const innerH = height - pad.t - pad.b; const max = Math.max(...data.map(d => d.FC + d.IEP)) * 1.1; const xStep = innerW / (data.length - 1); const y = v => pad.t + innerH - (v / max) * innerH; const fcPath = data.map((d, i) => `${i ? "L" : "M"}${pad.l + i * xStep},${y(d.FC)}`).join(" "); const totPath = data.map((d, i) => `${i ? "L" : "M"}${pad.l + i * xStep},${y(d.FC + d.IEP)}`).join(" "); const fcArea = fcPath + ` L${pad.l + (data.length - 1) * xStep},${pad.t + innerH} L${pad.l},${pad.t + innerH} Z`; return (
{[0, 0.25, 0.5, 0.75, 1].map((p, i) => ( {(max * p) >= 1000 ? ((max * p) / 1000).toFixed(0) + "k" : Math.round(max * p)} ))} {data.map((d, i) => i % 2 === 0 || i === data.length - 1 ? ( {fmtMonth(d.c, true)} ) : null)}
FC IEP
); } function InternacoesChart({ data, height = 220 }) { const ref = React.useRef(null); const [w, setW] = useStM(800); const [hover, setHover] = useStM(null); React.useEffect(() => { const obs = new ResizeObserver(([e]) => setW(e.contentRect.width)); if (ref.current) obs.observe(ref.current); return () => obs.disconnect(); }, []); const pad = { l: 44, r: 50, t: 16, b: 30 }; const innerW = Math.max(100, w - pad.l - pad.r); const innerH = height - pad.t - pad.b; const maxI = Math.max(1, ...data.map(d => d.internacoes)) * 1.1; const maxM = Math.max(0.001, ...data.map(d => d.mort)) * 1.2; const barW = innerW / data.length * 0.62; const yI = v => pad.t + innerH - (v / maxI) * innerH; const yM = v => pad.t + innerH - (v / maxM) * innerH; const xCenter = i => pad.l + (i + 0.5) * (innerW / data.length); const mortPath = data.map((d, i) => `${i ? "L" : "M"}${xCenter(i)},${yM(d.mort)}`).join(" "); const handleMouseMove = e => { const rect = ref.current?.getBoundingClientRect(); if (!rect) return; const mx = e.clientX - rect.left - pad.l; const idx = Math.floor(mx / (innerW / data.length)); setHover(idx >= 0 && idx < data.length ? idx : null); }; const hd = hover !== null ? data[hover] : null; const tooltipX = hover !== null ? Math.min(xCenter(hover) + 12, w - 155) : 0; return (
setHover(null)} style={{ cursor: "default" }}> {[0, 0.25, 0.5, 0.75, 1].map((p, i) => ( {Math.round(maxI * p / 1000) > 0 ? (maxI * p / 1000).toFixed(1).replace(".", ",") + "k" : Math.round(maxI * p)} {(maxM * p * 100).toFixed(1).replace(".", ",")}% ))} {data.map((d, i) => ( ))} {data.map((d, i) => ( ))} {data.map((d, i) => i % 2 === 0 || i === data.length - 1 ? ( {fmtMonth(d.c, true)} ) : null)} {hd && (
{fmtMonth(hd.c)}
Internações: {_fmt(hd.internacoes)}
Óbitos: {_fmt(hd.obitos ?? 0)}
Mortalidade: {_pct(hd.mort, 2)}
)}
); } Object.assign(window, { HospitaisView, ExamesView, InternacoesView });