const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "density": "regular", "formLayout": "sections", "headerStyle": "navy" }/*EDITMODE-END*/; function initialTheme() { try { const saved = localStorage.getItem("nz_theme"); if (saved === "dark" || saved === "light") return saved; } catch (e) {} try { if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) return "dark"; } catch (e) {} return "light"; } const DENSITY_SCALE = { compact: 0.92, regular: 1, comfy: 1.1 }; // Nomes de negócio dos três perfis de RF-USUARIOS-01 (ESP-0001). A chave é o // valor que a API devolve em `perfil`; o texto é o que a pessoa lê. const NOME_DO_PERFIL = { administrador: "Administração", editor: "Editoração", operador: "Operação", }; // Rotas privativas de `administrador` (RF-USUARIOS-05, RF-AUDITORIA-07, // RF-CONFIG-01). const AREA_ADMINISTRATIVA = ["users", "audit", "devices", "settings"]; function NavItem({ icon, label, count, active, onClick }) { return ( ); } function App() { const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); const [route, setRoute] = React.useState(() => { try { return JSON.parse(localStorage.getItem("nz_route")) || { view: "dashboard" }; } catch (e) { return { view: "dashboard" }; } }); const [toast, setToast] = React.useState(null); const [theme, setTheme] = React.useState(initialTheme); const [navOpen, setNavOpen] = React.useState(false); const [auth, setAuth] = React.useState({ loggedIn: false, user: null, loading: true }); const [loginError, setLoginError] = React.useState(null); const [loginBusy, setLoginBusy] = React.useState(false); React.useEffect(() => { if (window.API && window.API.isAuthenticated()) { window.API.getMe().then(u => { setAuth({ loggedIn: true, user: u, loading: false }); }).catch(() => { window.API.logout(); setAuth({ loggedIn: false, user: null, loading: false }); }); } else { setAuth(p => ({ ...p, loading: false })); } }, []); React.useEffect(() => { document.documentElement.style.setProperty("--ui-scale", DENSITY_SCALE[t.density] || 1); }, [t.density]); React.useEffect(() => { document.documentElement.setAttribute("data-theme", theme); try { localStorage.setItem("nz_theme", theme); } catch (e) {} }, [theme]); // RF-UI-01: `nz_route` é persistida em localStorage. Uma rota salva apontando // para a área de administração faria um editor que já foi administrador abrir // o portal numa tela proibida — que chamaria a API e receberia 403. Cair para // o painel corrige o estado guardado; a proteção continua sendo do backend. // // Fica aqui, junto dos demais hooks, e **antes** dos retornos antecipados // abaixo: um `useEffect` depois deles só é chamado em parte das renderizações, // e o React aborta com "rendered more hooks than during the previous render". const rotaProibida = auth.user ? auth.user.perfil !== "administrador" && AREA_ADMINISTRATIVA.includes(route.view) : false; React.useEffect(() => { if (rotaProibida) go("dashboard"); }, [rotaProibida]); const toggleTheme = () => setTheme(p => (p === "dark" ? "light" : "dark")); const go = (view, extra = {}) => { const nr = { view, ...extra }; setRoute(nr); setNavOpen(false); try { localStorage.setItem("nz_route", JSON.stringify(nr)); } catch (e) {} document.querySelector(".scroll")?.scrollTo(0, 0); }; const handleLogin = (email, pwd) => { setLoginBusy(true); setLoginError(null); window.API.login(email, pwd).then(() => { return window.API.getMe(); }).then(user => { setAuth({ loggedIn: true, user: user, loading: false }); setLoginBusy(false); go("dashboard"); }).catch(err => { setLoginError(err.message || "Erro ao autenticar"); setLoginBusy(false); }); }; const handleLogout = () => { window.API.logout(); setAuth({ loggedIn: false, user: null, loading: false }); go("login"); }; if (auth.loading) { return (
Carregando...
); } if (!auth.loggedIn) { return ( <> ); } // RF-AUTH-11: o diálogo abre imediatamente após o login e precede o portal // inteiro. Não é só cortesia — com senha provisória o backend recusa toda // requisição autenticada, e as telas atrás dele não teriam o que mostrar. if (auth.user.senha_provisoria) { return ( <> { // RF-AUTH-12: o token que trouxe a pessoa até aqui morreu na troca. // Deslogar é o único caminho — atualizar o estado em memória deixaria // a aplicação com uma credencial que o backend já recusa. setLoginError("Senha alterada. Entre novamente com a nova senha."); handleLogout(); }} /> ); } const user = auth.user; // RF-USUARIOS-01: três perfis, sem hierarquia. Sem valor de recurso — um // usuário sem perfil não é "editor por omissão"; o backend não emite mais // esse caso, e assumi-lo aqui daria à interface um poder que a API recusaria. const perfil = user.perfil || null; const ehAdministrador = perfil === "administrador"; const podeEditar = ehAdministrador || perfil === "editor"; const vista = rotaProibida ? "dashboard" : route.view; return (
setNavOpen(false)} aria-hidden="true">
{vista === "patient-form" ? ( setNavOpen(true)} onBack={() => go("patients")} onSave={(savedCase) => { go("patients"); setToast(savedCase.status === "published" ? "Caso publicado no acervo." : "Caso salvo como rascunho."); }} /> ) : (
setNavOpen(true)} />
{vista === "dashboard" && ( go("patients")} onOpenPatient={(id, extra = {}) => go("patient-form", { patientId: id, ...extra })} /> )} {vista === "patients" && ( go("patient-form", { patientId: id })} podeEditar={podeEditar} onNewPatient={() => go("patient-form", { patientId: null })} /> )} {vista === "users" && ( )} {vista === "audit" && ( )} {vista === "devices" && ( )} {vista === "settings" && ( )}
)} {toast && setToast(null)} />}
); } function TweaksPanelMount({ t, setTweak, theme, onToggleTheme }) { return ( setTweak("density", v)} /> setTweak("formLayout", v)} /> setTweak("headerStyle", v)} /> ); } ReactDOM.createRoot(document.getElementById("root")).render();