/* Área de gestão de usuários — RF-UI-01 e RF-USUARIOS-05 a 13 (ESP-0001). Vive dentro do shell, como `view_patients.jsx`, e não como takeover. Esconder a navegação não é proteger: a autorização é do backend (`/api/users` recusa quem não é administrador). A tela apenas evita oferecer o que seria recusado. */ const PERFIS = [ ["administrador", "Administração"], ["editor", "Editoração"], ["operador", "Operação"], ]; const NOME_DO_PERFIL = Object.fromEntries(PERFIS); const SENHA_MINIMA = 14; const ERROS_DE_USUARIO = { email_ja_cadastrado: "Já existe um usuário com este e-mail.", ultimo_administrador_ativo: null, // a mensagem do backend indica a saída; usar a dele autorrebaixamento_proibido: null, // idem — a mensagem do backend já orienta a procurar outro administrador perfil_invalido: "Selecione um perfil válido.", senha_fraca: `A senha deve ter no mínimo ${SENHA_MINIMA} caracteres.`, usuario_nao_encontrado: "Usuário não encontrado. Atualize a listagem.", }; function mensagemDeErro(erro) { if (!erro) return ""; const especifica = ERROS_DE_USUARIO[erro.code]; if (especifica) return especifica; return erro.message || "Não foi possível concluir a solicitação."; } function UserForm({ usuario, souEu, onSalvar, onCancelar, busy, erro }) { const novo = !usuario; const [nome, setNome] = React.useState(usuario ? usuario.nome : ""); const [email, setEmail] = React.useState(usuario ? usuario.email : ""); const [perfil, setPerfil] = React.useState(usuario ? usuario.perfil : "editor"); const [ativo, setAtivo] = React.useState(usuario ? usuario.ativo : true); const [senha, setSenha] = React.useState(""); const senhaCurta = novo && senha.length > 0 && senha.length < SENHA_MINIMA; const podeSalvar = nome.trim() && email.trim() && (!novo || senha.length >= SENHA_MINIMA); const submeter = () => { if (!podeSalvar || busy) return; // RF-USUARIOS-13: um administrador nunca rebaixa nem desativa a si mesmo — // o backend recusa com 403 de todo modo; aqui só evitamos enviar o campo // que já sabemos que será rejeitado. const dados = novo ? { nome: nome.trim(), email: email.trim(), perfil, ativo, senha } : { nome: nome.trim(), email: email.trim(), perfil, ativo }; onSalvar(dados); }; return (
{erro && {mensagemDeErro(erro)}} {souEu && ( Perfil e atividade não podem ser alterados por você mesmo — peça a outro administrador. )}
{novo && (
{/* RF-AUTH-07: o sistema não envia e-mail. A comunicação é externa e deliberadamente fora do escopo do software. */} O sistema não envia e-mail. A senha é provisória: o usuário será obrigado a trocá-la no primeiro acesso.
)}
Cancelar {busy ? "Salvando..." : (novo ? "Criar usuário" : "Salvar alterações")}
); } function ResetPasswordModal({ usuario, onConfirmar, onFechar, busy, erro }) { const [senha, setSenha] = React.useState(""); const curta = senha.length > 0 && senha.length < SENHA_MINIMA; return ( senha.length >= SENHA_MINIMA && onConfirmar(senha)} onClose={onFechar} > A senha nasce provisória e todos os acessos já feitos por esta pessoa deixam de valer. Comunique a nova senha por canal externo. ); } function UsersView({ usuarioCorrente, onToast }) { const [itens, setItens] = React.useState([]); const [total, setTotal] = React.useState(0); const [skip, setSkip] = React.useState(0); const [limit, setLimit] = React.useState(25); const [busca, setBusca] = React.useState(""); const [filtroPerfil, setFiltroPerfil] = React.useState(""); const [filtroAtivo, setFiltroAtivo] = React.useState(""); const [carregando, setCarregando] = React.useState(true); const [erroLista, setErroLista] = React.useState(null); const [editando, setEditando] = React.useState(null); // null | "novo" | usuário const [redefinindo, setRedefinindo] = React.useState(null); const [busy, setBusy] = React.useState(false); const [erroForm, setErroForm] = React.useState(null); const carregar = React.useCallback(() => { setCarregando(true); setErroLista(null); window.API.listUsers({ q: busca || undefined, perfil: filtroPerfil || undefined, ativo: filtroAtivo === "" ? undefined : filtroAtivo === "sim", skip, limit, }).then(dados => { setItens(dados.items || []); setTotal(dados.total || 0); setCarregando(false); }).catch(erro => { setErroLista(erro); setItens([]); setCarregando(false); }); }, [busca, filtroPerfil, filtroAtivo, skip, limit]); React.useEffect(() => { carregar(); }, [carregar]); const salvar = (dados) => { setBusy(true); setErroForm(null); const promessa = editando === "novo" ? window.API.createUser(dados) : window.API.updateUser(editando.id, dados); promessa.then(() => { setBusy(false); setEditando(null); onToast(editando === "novo" ? "Usuário criado." : "Usuário atualizado."); carregar(); }).catch(erro => { setBusy(false); setErroForm(erro); }); }; const redefinir = (senha) => { setBusy(true); setErroForm(null); window.API.resetPassword(redefinindo.id, senha).then(() => { setBusy(false); setRedefinindo(null); onToast("Senha redefinida. Comunique-a ao usuário."); carregar(); }).catch(erro => { setBusy(false); setErroForm(erro); }); }; const alternarAtividade = (usuario) => { setErroLista(null); window.API.updateUser(usuario.id, { ativo: !usuario.ativo }) .then(() => { onToast(usuario.ativo ? "Usuário desativado." : "Usuário ativado."); carregar(); }) .catch(erro => setErroLista(erro)); }; // RF-USUARIOS-16: exclusivo de administrador; a rota já exige o perfil, o // botão só existe para quem está bloqueado. const desbloquear = (usuario) => { setErroLista(null); window.API.unlockUser(usuario.id) .then(() => { onToast("Usuário desbloqueado."); carregar(); }) .catch(erro => setErroLista(erro)); }; if (editando) { return (
Administração

{editando === "novo" ? "Novo usuário" : editando.nome}

Perfil e atividade determinam o que esta pessoa alcança no portal.

{ setEditando(null); setErroForm(null); }} />
); } return (
Administração

Usuários

Quem acessa o portal e o que cada um pode fazer.

setEditando("novo")}>Novo usuário
{erroLista && ( setErroLista(null)}> {mensagemDeErro(erroLista)} )}
{[["", "Todos"], ...PERFIS].map(([valor, rotulo]) => ( ))}
{[["", "Ativos e inativos"], ["sim", "Ativos"], ["nao", "Inativos"]].map(([valor, rotulo]) => ( ))}
{ setBusca(e.target.value); setSkip(0); }} />
{carregando ? (
Carregando...
) : ( {itens.map(usuario => ( ))}
UsuárioPerfilAtividadeAções
{(usuario.nome || "?").split(" ").map(p => p[0]).slice(0, 2).join("")}
{usuario.nome} {usuario.id === usuarioCorrente.id && · você}
{usuario.email}
{NOME_DO_PERFIL[usuario.perfil] || usuario.perfil} {usuario.senha_provisoria && senha provisória} {usuario.bloqueado && bloqueado} {usuario.ativo ? "Ativo" : "Inativo"}
setEditando(usuario)} /> setRedefinindo(usuario)} /> {usuario.bloqueado && ( desbloquear(usuario)} /> )} {usuario.id !== usuarioCorrente.id && ( alternarAtividade(usuario)} /> )}
)} {!carregando && !itens.length && (
Nenhum usuário encontrado.
)}
{redefinindo && ( { setRedefinindo(null); setErroForm(null); }} /> )}
); } Object.assign(window, { UsersView, NOME_DO_PERFIL, PERFIS, SENHA_MINIMA });