// App principal: Login + Shell + roteamento de views + Tweaks
const { useState: useSt, useEffect: useEf, useRef: useRf } = React;
const ACCENT_PALETTES = {
verde: { c: "oklch(55% 0.085 165)", c2: "oklch(70% 0.06 165)", cs: "oklch(95% 0.025 165)", ci: "oklch(35% 0.06 165)", warn: "oklch(58% 0.12 45)", warnSoft: "oklch(95% 0.04 50)" },
azul: { c: "oklch(55% 0.10 240)", c2: "oklch(70% 0.07 240)", cs: "oklch(95% 0.03 240)", ci: "oklch(35% 0.07 240)", warn: "oklch(58% 0.12 45)", warnSoft: "oklch(95% 0.04 50)" },
ambar: { c: "oklch(58% 0.12 65)", c2: "oklch(72% 0.08 65)", cs: "oklch(95% 0.04 70)", ci: "oklch(38% 0.08 60)", warn: "oklch(56% 0.16 28)", warnSoft: "oklch(96% 0.03 28)" },
indigo: { c: "oklch(52% 0.13 290)", c2: "oklch(68% 0.08 290)", cs: "oklch(95% 0.035 290)", ci: "oklch(34% 0.09 290)", warn: "oklch(58% 0.12 45)", warnSoft: "oklch(95% 0.04 50)" },
};
function detectMobileDevice() {
const ua = navigator.userAgent || "";
const uaMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile/i.test(ua);
const narrow = window.matchMedia("(max-width: 760px)").matches;
const coarse = window.matchMedia("(pointer: coarse)").matches;
return narrow || (uaMobile && coarse);
}
function useDeviceClass() {
const [isMobile, setIsMobile] = useSt(false);
useEf(() => {
function update() {
const next = detectMobileDevice();
document.body.dataset.device = next ? "mobile" : "desktop";
setIsMobile(next);
}
update();
window.addEventListener("resize", update);
window.addEventListener("orientationchange", update);
return () => {
window.removeEventListener("resize", update);
window.removeEventListener("orientationchange", update);
};
}, []);
return isMobile;
}
// ─── Loading screen ────────────────────────────────────────────
function LoadingScreen({ error, onRetry }) {
return (
{error ? (
<>
{error}
>
) : (
Carregando dados…
)}
);
}
// ─── Login ────────────────────────────────────────────────────
function LoginScreen({ onLogin }) {
const [email, setEmail] = useSt("ana.marketing@farma.io");
const [pass, setPass] = useSt("••••••••");
const [busy, setBusy] = useSt(false);
function submit(e) {
e.preventDefault();
if (!email || !pass) return;
setBusy(true);
setTimeout(() => onLogin({ email }), 650);
}
const yyyy = new Date().getFullYear();
return (
Dados que viram decisão.
Não relatório.
Plataforma de inteligência farmacêutica para times de marketing — mapeie demanda, antecipe rupturas e entenda a jornada de cada paciente.
{yyyy} · farma - aulerlabs product
);
}
// ─── Drug picker ─────────────────────────────────────────────
function DrugPicker({ drug, onChange }) {
const [open, setOpen] = useSt(false);
const ref = useRf(null);
useEf(() => {
function click(e) { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }
document.addEventListener("mousedown", click);
return () => document.removeEventListener("mousedown", click);
}, []);
const drugs = window.FARMA_DATA.drugs;
return (
{open && (
{drugs.map(d => (
))}
)}
);
}
// ─── Dashboard shell ─────────────────────────────────────────
function Dashboard({ user, onLogout, initialView = "demanda", showInsight = true, isMobile = false }) {
const [drug, setDrug] = useSt(window.FARMA_DATA.drugs[0]);
const [view, setView] = useSt(initialView);
const [market, setMarket] = useSt("Todos");
const [period, setPeriod] = useSt("M");
// Sync date from data
const syncDate = window.FARMA_DATA.kpis?.competencia || "—";
const syncShort = syncDate.split("/").map(s => s.trim()).reverse().join("/").slice(0, 7);
const initials = user.email.split("@")[0].split(".").map(s => s[0]).join("").slice(0, 2).toUpperCase();
const navItems = [
{ id: "demanda", label: "Demanda", icon: NavIconMap, count: window.FARMA_DATA.kpis?.ufsAtendidas + " UFs" },
{ id: "pacientes", label: "Pacientes", icon: NavIconUsers, count: (window.FARMA_DATA.kpis?.pacientesAtivos / 1000).toFixed(1).replace(".", ",") + "k" },
{ id: "hospitais", label: isMobile ? "Hosp." : "Hospitais", icon: NavIconHospital, count: window.FARMA_DATA.kpis?.estabsAtivos },
{ id: "exames", label: "Exames", icon: NavIconFlask, count: "20k" },
{ id: "ied", label: isMobile ? "Intern." : "Internações IEP", icon: NavIconHeart, count: (window.FARMA_DATA.internacoes?.total / 1000).toFixed(1).replace(".", ",") + "k" },
];
const periodOptions = isMobile
? [{ id: "M", label: "Mês" }, { id: "3M", label: "3m" }, { id: "12M", label: "12m" }, { id: "YTD", label: "YTD" }]
: [{ id: "M", label: "Mês corrente" }, { id: "3M", label: "3 meses" }, { id: "12M", label: "12 meses" }, { id: "YTD", label: "YTD" }];
return (
Painéis/
{drug.name}/
{{ demanda: "Demanda", pacientes: "Pacientes", hospitais: "Hospitais", exames: "Exames", ied: "Internações IEP" }[view] || "—"}
{initials}
FILTRAR
{["Todos", "Fibrose Cística", "IEP"].map(m => (
))}
{periodOptions.map(p => (
))}
{view === "demanda" && }
{view === "pacientes" && }
{view === "hospitais" && }
{view === "exames" && }
{view === "ied" && }
);
}
// ─── Ícones ───────────────────────────────────────────────────
function NavIconMap() { return ; }
function NavIconUsers() { return ; }
function NavIconHospital(){ return ; }
function NavIconFlask() { return ; }
function NavIconHeart() { return ; }
function NavIconCog() { return ; }
function NavIconLogout() { return ; }
// ─── App root ─────────────────────────────────────────────────
function App() {
const [t, setTweak] = useTweaks(window.TWEAK_DEFAULTS);
const [user, setUser] = useSt(null);
const [toast, setToast] = useSt(null);
const [ready, setReady] = useSt(!window.__FARMA_LOADING);
const [loadError, setLoadError] = useSt(null);
const isMobile = useDeviceClass();
// Aguarda dados da API
useEf(() => {
if (ready) return;
function onReady() { setReady(true); }
window.addEventListener("farma:ready", onReady);
return () => window.removeEventListener("farma:ready", onReady);
}, [ready]);
// Aplica tweaks como CSS variables
useEf(() => {
const root = document.documentElement.style;
const p = ACCENT_PALETTES[t.accent] || ACCENT_PALETTES.verde;
root.setProperty("--accent", p.c);
root.setProperty("--accent-2", p.c2);
root.setProperty("--accent-soft", p.cs);
root.setProperty("--accent-ink", p.ci);
root.setProperty("--r-lg", t.radius + "px");
root.setProperty("--r-md", Math.max(6, t.radius - 4) + "px");
root.setProperty("--r-sm", Math.max(4, t.radius - 8) + "px");
root.setProperty("--display-weight", String(t.displayWeight));
document.body.dataset.density = t.density;
}, [t.accent, t.radius, t.displayWeight, t.density]);
useEf(() => {
if (toast) {
const tm = setTimeout(() => setToast(null), 2200);
return () => clearTimeout(tm);
}
}, [toast]);
if (!ready) {
return { window.location.reload(); }} />;
}
if (!user) return (
<>
{ setUser(u); setToast("Bem-vinda, " + u.email.split("@")[0].split(".")[0]); }} />
>
);
return (
<>
setUser(null)} initialView={t.defaultView} showInsight={t.showInsight} isMobile={isMobile} />
{toast && {toast}
}
>
);
}
function FarmaTweaks({ t, setTweak }) {
return (
setTweak("accent", v)} />
setTweak("density", v)} />
setTweak("radius", v)} />
setTweak("displayWeight", v)} />
setTweak("showInsight", v)} />
setTweak("defaultView", v)} />
);
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render();