// 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)}
▲ 12 desde dez/25
Em ruptura
{ruptCount + 45}
▼ 3 vs. mês anterior
Em atenção
{atencao + 70}
0,6× — 0,85× da média
Crescendo >10%
{growing + 110}
oportunidade de expansão
{[
{ 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 => (
setStatusFilter(f.id)}>
{f.tone && }
{f.label}
))}
{["Todos", "FC", "IEP", "Mix"].map(m => (
setMarketFilter(m)}>{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 => (
setSort(s.id)} style={{ fontSize: 11.5, padding: "5px 10px" }}>
{s.label}
))}
{list.slice(0, 20).map(h => {
const tone = h.indicator < 0.6 ? "danger" : h.indicator < 0.85 ? "warn" : "ok";
const delta = trendDelta(h);
return (
setSelected(h.cnes)}>
{h.name}
{h.city} · CNES {h.cnes}
{_fmt(h.patients)} pac.
{_fmtCompact(h.caixas)} cx/mês
= 0 ? "var(--accent-ink)" : "var(--danger)" }}>
{delta >= 0 ? "+" : ""}{(delta * 100).toFixed(0)}% 6m
{h.uf}
= 0 ? "var(--accent)" : "var(--danger)"} />
{tone === "danger" ? "Ruptura" : tone === "warn" ? "Atenção" : "Saudável"}
);
})}
{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 => (
setMarketFilter(m)}>{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ório UF Volume Mercado
{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}
{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,08pp vs. 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.
# Procedimento Internações Mortalidade Potencial Volume 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.
# Hospital UF Internações Mortalidade Volume
{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 });