// Visões principais: Demanda (mapa + hospitais) e Pacientes (série + funil + coorte)
const { useState: useS, useMemo: useM } = 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 n.toString();
};
// ─── Visão Demanda ────────────────────────────────────────────
function DemandaView({ market, period, showInsight = true }) {
const { stateData, hospitals, kpis, monthly } = window.FARMA_DATA;
const [metric, setMetric] = useS("dispensed");
const [selectedUF, setSelectedUF] = useS("SP");
const stateSel = stateData.find(s => s.uf === selectedUF);
const hospitalsInState = hospitals.filter(h => h.uf === selectedUF);
const hospitalsToShow = useM(() => {
const base = selectedUF ? hospitalsInState : hospitals;
return base.slice(0, 8);
}, [selectedUF]);
const sparkAtivos = monthly.slice(-12).map(m => m.ativos);
const sparkCaixas = monthly.slice(-12).map(m => m.caixas);
const ufRuptura = stateData.filter(s => s.ruptureRate >= 0.18).sort((a, b) => b.ruptureRate - a.ruptureRate);
const topRupturaUF = ufRuptura[0];
return (
<>
Demanda & abastecimento · {kpis.competencia}
Onde a Pancreatina é dispensada
Cada quadrado é um estado. Cor mais intensa = mais demanda. Pontos amarelos sinalizam UFs onde 18% ou mais dos estabelecimentos estão em ruptura.
setMetric("dispensed")}>Cápsulas
setMetric("patients")}>Pacientes
setMetric("ruptureRate")}>Ruptura
{showInsight && topRupturaUF && (
!
{topRupturaUF.name} ({topRupturaUF.uf}) lidera ruptura nesta competência —{" "}
{topRupturaUF.ruptureEstabs} de {topRupturaUF.estabs} estabelecimentos com dispensação abaixo de 60% da média de 6 meses.
)}
Cápsulas dispensadas — {kpis.competencia}
{fmtCompact(kpis.capsulas)}cáps.
▲ 2,3%
vs. mês anterior
Caixas / mês
{fmtCompact(kpis.caixas)}
▲ 2,3% vs. mês anterior
Estabelecimentos ativos
{fmt(kpis.estabsAtivos)}
▲ 12 desde dezembro
Em ruptura
{kpis.estabsRuptura}
{((kpis.estabsRuptura / kpis.estabsAtivos) * 100).toFixed(1).replace(".", ",")}%
▼ 3 estab. vs. mês anterior
Brasil em quadrados
Clique em um estado para detalhar.
{kpis.ufsAtendidas} UFs · {kpis.estabsAtivos} estab.
Estado selecionado
{stateSel.name}{stateSel.uf}
{stateSel.region === "SE" ? "Sudeste" : stateSel.region === "S" ? "Sul" : stateSel.region === "NE" ? "Nordeste" : stateSel.region === "N" ? "Norte" : "Centro-Oeste"}
Pacientes ativos
{fmt(stateSel.patients)}
{((stateSel.patients / kpis.pacientesAtivos) * 100).toFixed(1).replace(".", ",")}% do total
Cápsulas / mês
{fmtCompact(stateSel.dispensed)}
{fmt(stateSel.caixas)} caixas
Estabelecimentos
{stateSel.estabs}
{stateSel.ruptureEstabs} em ruptura
Taxa de ruptura
0.15 ? "warn" : "")}>
{(stateSel.ruptureRate * 100).toFixed(0)}%
{stateSel.ruptureRate > 0.2 ? "Atenção crítica" : stateSel.ruptureRate > 0.1 ? "Monitorar" : "Saudável"}
Dispensação × Ruptura no tempo
Caixas dispensadas por mês vs. taxa de ruptura nacional.
Caixas
Ruptura
Hospitais {selectedUF ? `em ${stateSel.name}` : "no Brasil"}
Ordenados por dispensação. Barra = indicador qt_atual ÷ média 6m.
# Estabelecimento UF Dispensação vs. média 6m Status
{(hospitalsToShow.length ? hospitalsToShow : hospitals.slice(0, 8)).map((h, i) => {
let text = "Saudável", tone = "ok";
if (h.indicator === 0 || h.indicator < 0.6) { text = "Ruptura"; tone = "danger"; }
else if (h.indicator < 0.85) { text = "Atenção"; tone = "warn"; }
return (
{String(i + 1).padStart(2, "0")}
{h.name}
{h.city} · CNES {h.cnes} · {fmt(h.patients)} pac · {fmt(h.caixas)} caixas
{h.uf}
{text}
);
})}
{selectedUF && hospitalsInState.length === 0 && (
Sem hospitais no top 20 para {stateSel.name}.{" "}
setSelectedUF("SP")} style={{ background: "transparent", border: 0, color: "var(--accent-ink)", cursor: "pointer", textDecoration: "underline" }}>
Ver Brasil
)}
>
);
}
// ─── Visão Pacientes ──────────────────────────────────────────
function PacientesView() {
const { monthly, markets, cohort, dropoutReasons, kpis } = window.FARMA_DATA;
const last = monthly[monthly.length - 1];
const prev = monthly[monthly.length - 2];
const deltaAtivos = ((last.ativos - prev.ativos) / prev.ativos) * 100;
const yearAgo = monthly[monthly.length - 13];
const totalPatients = markets.reduce((a, m) => a + m.patients, 0);
const funnel = [
{ label: "Novos tratamentos em 2025", value: kpis.naive2025, tone: "green" },
{ label: "Pacientes ativos hoje", value: kpis.pacientesAtivos, tone: "" },
{ label: "Retornaram após desistência", value: kpis.retorno + 84, tone: "green" },
{ label: "Desistências neste mês", value: kpis.dropoutMes, tone: "warn" },
];
return (
<>
Jornada de pacientes · {kpis.competencia}
{fmt(last.ativos)} pessoas em tratamento
Quem entrou, quem ficou e quem abandonou — desde a regulamentação pelo SUS em julho de 2025.
24 meses
12 meses
YTD
Pacientes ativos
{fmt(last.ativos)}
▲ {deltaAtivos.toFixed(1).replace(".", ",")}%
vs. mês anterior
Novos no mês
+{fmt(last.novos)}
{fmt(kpis.naive2026)} desde jan/26
Desistências no mês
−{last.dropout}
▼ 4 vs. mês anterior
Compliance médio
{(kpis.complianceRate * 100).toFixed(1).replace(".", ",")}%
{kpis.tempoMedioDias} dias entre dispensações
Pacientes ativos ao longo do tempo
Entrada de cobertura SUS em julho/2025 expandiu o mercado em ~120×.
{fmtMonth(monthly[0].c, true)} → {fmtMonth(monthly[monthly.length - 1].c, true)}
Funil do tratamento
Da primeira dispensação ao abandono — última competência.
Desistência = 4+ meses sem dispensação
Taxa: {((kpis.dropoutMes / kpis.pacientesAtivos) * 100).toFixed(2).replace(".", ",")}%
Retenção por coorte
% dos pacientes que iniciaram em 2025 e continuam ativos.
{cohort[cohort.length - 1].pct}% ativos após 12 meses
Por mercado terapêutico
Fibrose cística e insuficiência exócrina pancreática.
{markets.map(m => (
{m.name}
CID {m.cids.join(" · ")}
{fmt(m.patients)}
Novos 2025 {fmt(m.naive2025)}
Share {((m.patients / totalPatients) * 100).toFixed(0)}%
))}
Por que abandonam?
Distribuição dos motivos de desistência.
>
);
}
Object.assign(window, { DemandaView, PacientesView });