20 KiB
Perfis de Acesso / Usuários (telas administrativas)
Movido do
CLAUDE.mdda raiz em 2026-08-26 para reduzir conflito de edição entre aplicações. VerCLAUDE.mdna raiz para o modelo de permissões em si (formato do JSON dePerfilAcesso.permissoes, padrão visualizar/editar,permissoes_efetivas()) — esse conteúdo é transversal e continua lá porque toda aplicação do Portal depende dele. Este arquivo cobre só as telas administrativas (perfis-acesso.html/usuarios.html) em si. Não é auto-carregado pelo Claude Code — leia manualmente ao mexer nessas duas telas.
Popup "Nova Aplicação" (gerenciar acesso por aplicação, entre perfis)
Botão #pa-app-search-btn ao lado de "Novo Perfil" (perfis-acesso.html) abre #pa-app-search-modal — o caminho inverso da árvore de permissões: em vez de abrir um perfil e marcar módulo por módulo, o usuário busca uma aplicação/ferramenta pelo nome e vê/gerencia todos os perfis que têm acesso a ela numa tabela só.
- Fonte dos dados — 100% reaproveitado, sem endpoint novo:
pidFlattenAplicacoes()(profiles.js) achataPID_MODULES×PID_MODULE_APPS(já carregados deGET /api/catalogo/no load da página) numa lista plana de "aplicações" — uma por entrada deMODULE_APPS, seja ela um app simples ou um subgrupo comtoolsaninhadas (mesma unidade usada emrenderEntry()da árvore). A busca (renderAppSearchResults()) casa o termo contra o label da aplicação, o label do módulo e o label de cadatoolaninhada — esse último é o que permite achar, por exemplo, "Controle Simples Nacional" (otoolreal dentro do subgrupo "Consultoria Tributária" de Auditorias) mesmo a unidade selecionável sendo o subgrupo inteiro. - Painel de gerenciamento (
renderAppManageTable()): ao clicar num resultado, mostra uma tabela com uma linha por perfil (profiles, o mesmo array já carregado pela tela) e uma coluna de checkbox portool— para app simples sem subgrupo, uma coluna única "Acesso". O cabeçalho de cada coluna usatool.label.split(" (")[0](corta o parêntese explicativo tipo "Editar (reordenar, incluir e remover cartões)" → "Editar"), sem precisar de um label curto dedicado no catálogo. - Salva na hora, por checkbox (decisão explícita do usuário — sem botão "Salvar" no popup): cada
changedisparaPATCH /api/perfis/{codigo}/só com{permissoes: perfil.permissions}(pidUpdatePerfilPermissoes, PATCH parcial — oModelViewSetjá aceita,PerfilAcessoSerializernão exige os outros campos fora departial_update). Erro de rede reverte o checkbox e o estado em memória, compidAlert()(ver "Modal de confirmação genérico" noCLAUDE.mdda raiz — mesmo padrão de outros toggles imediatos do app, ex. inativar usuário). - Conceder acesso habilita o módulo automaticamente (decisão explícita do usuário): se o checkbox marcado pertence a um módulo com
enabled=falsenaquele perfil, o toggle também viraperm.enabled = trueno mesmo PATCH — sem isso, o perfil ganharia a chave emappsmas o item continuaria escondido no menu (access.jsesconde onav-group/nav-subiteminteiro porenabled, não só por app). Revogar não desabilita o módulo de volta (outras aplicações dele podem seguir em uso por aquele perfil). - Perfis inativos (
ativo=False) aparecem na tabela com o selo.status-pill--inativo(mesmo componente da coluna "Status" deusuarios.html), sem serem excluídos da lista — nada nesse popup impede gerenciar o acesso deles.
Aba "Usuários do Escritório" (dentro da edição de um perfil) — duas tabelas com seleção múltipla
Substituiu o antigo <select> + botão "Vincular" + lista simples com X pra remover — pedido explícito do usuário pra reestruturar visualmente no estilo de um componente de transferência dupla (referência: uma tela de outro sistema com duas grades lado a lado, cada uma com checkbox de seleção, busca por coluna, ordenação e um botão de ação em lote).
- Duas tabelas (
.pa-users-dual, grid 2 colunas que colapsa pra 1 abaixo de 900px): à esquerda,#pa-users-available-*— todo usuário ativo ainda não vinculado a este perfil; à direita,#pa-users-linked-*— todo usuário ativo já vinculado. Usuário inativo nunca aparece em nenhum dos dois painéis (listaParaPainelUsuarios()filtrausuariosCacheAtualporis_activeantes de separar entre vinculado/disponível) — decisão explícita do usuário; na prática, inativar já limpa osperfisde alguém no backend (_revogar_acesso_se_inativo(), ver "Inativar/reativar usuário" abaixo), então esse filtro no frontend é sobretudo defensivo pra dados legados. Cada tabela tem: checkbox de seleção por linha + "selecionar todos" no cabeçalho (#pa-users-available-select-all/#pa-users-linked-select-all, aplica só sobre as linhas filtradas visíveis, mesmo critério de.checklist-select-all), coluna "Nome Usuário" ordenável (clique alterna asc/desc, ícone↕que fica--accentquando ativo — mesmo padrão.ua-sort-iconjá usado emusuarios.html) e coluna "E-mail" (não ordenável), com uma segunda linha de cabeçalho (.pa-users-table__filters) só com os campos de busca por nome/e-mail — filtro client-side sobre o array já carregado, sem debounce. - Botão de atualizar (ícone circular,
#pa-users-available-refresh-btn/#pa-users-linked-refresh-btn) refazGET /api/usuarios/(refreshUsuarios(),profiles.js) e re-renderiza os dois painéis a partir do mesmo cache — as duas tabelas sempre refletem o mesmo snapshot de usuários, nunca buscam independentemente uma da outra. - Vincular/Desvincular em lote: o botão de cada painel (
#pa-users-link-btn/#pa-users-unlink-btn, desabilitado enquanto a seleção daquele painel estiver vazia) disparabulkAlterarVinculo(kind, vincular)— umPATCH /api/usuarios/{id}/(pidSetUsuarioPerfis) por usuário selecionado, em paralelo (Promise.all), cada um recalculando a própria lista deperfis(adiciona ou remove só ocodigodo perfil sendo editado, preservando os demais perfis do usuário). Ao terminar, a seleção é limpa e os dois painéis são recarregados do zero (refreshUsuarios()) — um usuário que acabou de ser vinculado desaparece da tabela da esquerda e aparece na da direita, e vice-versa. - Abrir a aba de um perfil diferente (
renderUsers(), chamada poropenEdit()) sempre reseta os dois painéis: seleção limpa, ordenação de volta pra ascendente, campos de busca vazios — evita carregar o estado de filtro/seleção deixado num perfil anterior. - Sem endpoint novo — 100% reaproveitamento de
GET /api/usuarios/(UsuarioListSerializer, já expõeemail) ePATCH /api/usuarios/{id}/(pidSetUsuarioPerfis, já existia).
Inativar/reativar usuário (usuarios.html)
Usa o campo is_active que já vem de AbstractUser — não foi criado nenhum campo/migração novo, só exposto em UsuarioSerializer/UsuarioListSerializer e ligado na UI. is_active=False já é suficiente pro Django bloquear o acesso sozinho, sem nenhum código extra de autenticação:
- Login novo:
authenticate()(usado emlogin_view) roda viadjango.contrib.auth.backends.ModelBackend, que internamente chamauser_can_authenticate()e recusa (None) qualquer usuário comis_active=False, mesmo com a senha certa. Como isso fazauthenticate()retornarNonetanto pra senha errada quanto pra usuário inativo,login_viewfaz uma checagem manual só no caminho de falha (Usuario.objects.filter(username=username, is_active=False).first()+check_password()) pra devolver uma mensagem diferente ("Este usuário está inativo...", 403) só quando a senha bate mas a conta está inativa — sem essa checagem extra, qualquer tentativa com credenciais erradas ou inexistentes continua caindo no genérico "Login ou senha inválidos." (401), pra não revelar se um username existe. - Sessão já aberta: também não precisa de nenhum middleware/permissão customizado —
ModelBackend.get_user(user_id)(chamado pelo Django a cada request pra popularrequest.usera partir da sessão) também recusa usuários inativos, então na próxima requisição depois de desativado o usuário viraAnonymousUserautomaticamente eIsAuthenticated/PodeGerenciarPermissoesjá barram sozinhos. Ou seja: desativar alguém já derruba o acesso na mesma hora, não só impede o próximo login.
Inativar desvincula perfis de acesso e liderança automaticamente (_revogar_acesso_se_inativo(), serializers.py, decisão explícita do usuário): sempre que UsuarioSerializer.create()/update() termina com usuario.is_active=False, perfis é limpo (usuario.perfis.clear()) e o usuário sai do liderados de qualquer gerente que o tivesse (usuario.lideres.clear() — lideres é a relação reversa de Usuario.liderados; limpar aqui remove usuario do lado de quem o lidera, sem afetar quem usuario eventualmente lidera, caso ele mesmo seja gerente). A chamada é sempre a última coisa em create()/update(), depois dos .set() de perfis/departamentos/liderados — colocar antes seria inútil, já que o formulário de edição de usuarios.html sempre reenvia o checklist de perfis inteiro junto com qualquer mudança no checkbox "Usuário ativo", e um .set() posterior desfaria uma limpeza feita cedo demais. Cobre os dois pontos de entrada reais (botão de alternar na lista + checkbox no formulário de edição), ambos passando por PATCH /api/usuarios/{id}/; não há um hook equivalente no admin.py (uso interno, fora de escopo). Não é uma trava: nada impede reativar alguém depois (ele volta sem nenhum perfil/liderança, precisa reconfigurar) nem impede — por ora — que um gerente adicione manualmente um usuário já inativo aos próprios liderados pela tela dele (o hook só dispara ao salvar o usuário inativo em si, não ao salvar o gerente).
Na UI (users-admin.js/usuarios.html): coluna "Status" na lista (.status-pill/.status-pill--ativo/.status-pill--inativo, mesmo componente que já existia pro PerfilAcesso.ativo) e um botão de alternar (ícone de "power") na linha, ao lado de editar/excluir — PATCH /api/usuarios/{id}/ com {is_active: !atual}, com confirmação via window.confirm. O checkbox "Usuário ativo" no formulário de edição faz a mesma coisa (útil quando já se está editando outros campos). Os dois lugares bloqueiam auto-desativação (mesmo padrão de guarda já usado pra "não pode excluir a si mesmo": checagem só no frontend, id === me.id) — no formulário isso aparece como o checkbox desabilitado (disabled) quando account.id === me.id, em vez de um alerta.
Filtro de status na lista (#ua-status-filtros, chips "Ativos"/"Inativos"/"Todos" ao lado do título "Usuários" — mesma linguagem visual de .ind-departamento-chip): client-side, sobre o array users já carregado (statusFiltro em users-admin.js, aplicado em renderList() antes do filtro de busca por texto). Nasce em "ativos" por padrão (decisão explícita do usuário — a lista não deve abrir mostrando quem já foi desativado) e reseta pra "ativos" só no load da página, não a cada renderList().
Colunas de código cadastral + ordenação (#ua-table): a lista também mostra codigo_folha/codigo_questor/codigo_tareffa/codigo_contabit/ramal como colunas próprias (antes só apareciam dentro do formulário de edição) — pedido explícito do usuário pra conseguir achar quem está sem algum desses códigos cadastrado antes de outras aplicações passarem a depender deles, mesmo eles sendo campos opcionais (blank=True) no model. Célula vazia renderiza <span class="ua-campo-vazio">—</span> (itálico, cor apagada) em vez de string vazia, pra ficar visualmente óbvio ao ordenar a coluna. Todo <th data-sort="..."> (login, nome, os 4 códigos, ramal, status) é clicável e alterna asc/desc (sortKey/sortDir em users-admin.js, ícone ↕ que fica --accent quando ativo) — mesmo padrão de #ips-list-table (importacao-plano-saude.js, ver portal_api/planos_saude/CLAUDE.md) e do modal de Ramais (ramais-lookup.js, ver docs/ramais.md), inclusive a mesma função de comparação (comparaValoresUsuario, número vs. número quando os dois convertem, senão localeCompare pt-BR) — cada arquivo mantém sua própria cópia da função, não foi extraída pra um utilitário compartilhado em api.js. "Perfil de Acesso" (junção de nomes) não é ordenável, mesmo critério das outras telas que não ordenam colunas agregadas.
Botão "Vincular" (visual, sem funcionalidade ainda) nos campos Código da Folha/Questor/Tareffa do formulário de edição: decisão explícita do usuário — esses 3 códigos vão futuramente ser buscados/vinculados a partir de uma ferramenta externa (ex.: codigo_tareffa via a view já existente em portal_api/database/ que lê o Tareffa, ver project_database_package na memória) em vez de digitados à mão, mas essa vinculação de verdade não foi implementada nesta rodada — só a estrutura visual. Cada um dos 3 campos (#ua-codigo-folha/#ua-codigo-questor/#ua-codigo-tareffa) ganhou um input + botão "Vincular" (ícone de elo + texto) encostados numa única caixa (.ua-field-link — borda/raio únicos, botão separado por border-left, mesmo estilo de referência que o usuário mostrou de um campo de busca com botão "Buscar" atado à direita); passou por duas versões mais simples antes (botão solto ao lado do input, depois só o ícone sem texto no canto) até o usuário pedir essa terceira, "no estilo do botão de buscar". Sempre disabled com title="Vinculação com sistema externo ainda não implementada" — não tem nenhum handler de clique em users-admin.js. codigo_contabit e ramal (também campos cadastrais na mesma seção "Dados Cadastrais") não ganharam o botão — não fazem parte do conjunto de códigos com vinculação externa planejada, continuam sendo só texto livre. Ao implementar a busca de verdade num momento futuro, reaproveitar esse mesmo botão (tirar o disabled, adicionar o handler), não recriar o campo do zero.
Liderança (gerente/coordenador) e o modal "Gerenciar Usuário"
Usuario.lideranca (booleano) marca um usuário como gerente/coordenador de outros; Usuario.liderados é um M2M auto-referenciado ("self", symmetrical=False, related_name="lideres") — ou seja, "A lidera B" não implica "B lidera A". Exemplo: marcar lideranca=True em "debora" e incluir "gabriel" em liderados representa "debora é gerente de gabriel".
Dois lugares gravam essa mesma relação:
- Tela de Usuários (
usuarios.html/users-admin.js, só quem temgerencia_permissoes): seção "Liderança" no formulário de edição — checkbox "É gerente ou coordenador de outros usuários" (#ua-lideranca) libera (hidden) o widget de vinculação dual descrito abaixo (a própria conta sendo editada é excluída da lista de candidatos — mesmo padrão de guarda "frontend-only" já usado pra "não pode excluir a si mesmo"/"não pode se auto-desativar", não há checagem equivalente no backend). Salvar envialideranca+liderados(array de ids) no mesmo payload dePATCH/POST /api/usuarios/. - Modal "Gerenciar Usuário" (
account.js, disponível em todo shell via o item "Gerenciar Usuário" no dropdown da conta — substituiu o antigo botão direto "Alterar senha"): abre#manage-account-modal, que sempre tem um botão "Alterar senha" (que fecha esse modal e abre o#password-modaljá existente, mesmo fluxo de antes) e, só seme.liderancafortrue, o mesmo widget de vinculação dual — permitindo que o próprio gerente/coordenador se autogerencie sem precisar de acesso à tela administrativa de Usuários. Essa lista vem deGET /api/usuarios-resumo/(não de/api/usuarios/, que exigegerencia_permissoes) e salvar disparaPATCH /api/me/liderados/, que grava na mesmaUsuario.liderados—meus_liderados_viewrecusa (403) serequest.user.liderancaforFalse, já que só faz sentido pra quem tem o checkbox marcado.
Widget "Usuários sob liderança" — duas tabelas, não vinculados à esquerda e vinculados à direita (.dual-select, static/js/dual-select.js + estilos em components.css): substituiu o antigo .checklist-box de uma lista só (checkbox + busca + "marcar todos") — reestruturado a pedido do usuário no mesmo estilo do widget "Usuários do Escritório" de Perfis de Acesso (ver acima), só que genérico o bastante pra rodar tanto em usuarios.html quanto dentro do modal "Gerenciar Usuário" (presente em todo shell). pidCriarSeletorDuplo(config) (dual-select.js, incluído no prefixo de scripts de todo shell, logo depois de api.js) é a fábrica compartilhada — recebe as referências de DOM de cada painel (available/linked: checkbox "selecionar todos", cabeçalho ordenável, dois campos de busca, corpo da tabela, rodapé de contagem e o botão de ação) mais secundariaValor(candidato) (aqui, departamentosTexto(), unindo os nomes dos departamentos por vírgula) e devolve { setDados(candidatos, vinculadosIniciais), getVinculadosIds() }. Cada tela (users-admin.js/account.js) só chama setDados() ao abrir o formulário/modal e getVinculadosIds() no momento de salvar — a vinculação em si é só em memória dentro do widget (nenhuma chamada de API própria), o "Vincular"/"Desvincular" só move ids entre os dois painéis local mente, igual ao checklist antigo (que também só populava um Set em memória até o "Salvar" de fora).
- Cada painel tem: checkbox de seleção múltipla + "selecionar todos" (sobre as linhas filtradas visíveis, mesmo critério do antigo
.checklist-select-all), coluna "Nome" ordenável (clique alterna asc/desc) e coluna "Departamento", com uma segunda linha de cabeçalho só com os dois campos de busca (por nome e por departamento, independentes) — filtro client-side sobre o array já carregado. O botão de ação do painel ("Vincular" a esquerda/"Desvincular" a direita) fica desabilitado enquanto a seleção daquele painel estiver vazia, e mover usuários limpa a seleção e re-renderiza os dois painéis (quem saiu de um painel aparece no outro). #manage-account-modal-card(id novo no.modal-carddo modal "Gerenciar Usuário") ganha a classe.modal-card--widevia JS (account.js) só quandome.liderancaétrue— o modal volta ao tamanho padrão (420px) quando só tem o botão "Alterar senha", em vez de ficar largo à toa pra quem não lidera ninguém..dual-select/.dual-select__*moram emcomponents.css(não numa folha de estilo dedicada), pela mesma razão de.checklist-box— o modal "Gerenciar Usuário" existe em todo shell, e a maioria deles não carregaperfis-acesso.css.- Usuário inativo nunca aparece nos candidatos — mesma decisão de "Usuários do Escritório" acima. Em
usuarios.html,fillLideradosChecklist()(users-admin.js) filtrausersporis_activeantes de montar a lista de candidatos; no modal "Gerenciar Usuário", isso já vem de graça porqueGET /api/usuarios-resumo/(usuarios_resumo_view) só devolve usuários ativos.
seed_portal.py não reseta nome de um perfil já existente
Bug real corrigido, motivado pelo usuário ter renomeado o perfil "Integração e Inovação" código 8 pra "Inovação" — e criado um perfil novo "Integração", código 9: update_or_create(codigo=..., defaults={"nome": ..., ...}) reescrevia nome a cada execução, revertendo qualquer renomeação feita depois pela tela de Perfis de Acesso. Trocado por get_or_create(codigo=..., defaults={"nome": ...}) (nome só gravado na criação) + atribuição direta de ativo/gerencia_permissoes/permissoes a cada execução (esses continuam sendo realinhados sempre — é assim que o seed serve pra corrigir uma árvore de permissões corrompida/desatualizada, só nome parou de ser tocado). Mesmo espírito de "só inicializa na primeira criação" já usado pra gabriel/bruno. Ver [[feedback_seed_nao_reseta_gabriel_bruno]] na memória.
CSS
perfis-acesso.css (.pa-*) cobre as duas telas — inclui as seções (.ua-section*) e campos específicos (.ua-inline-add/.ua-departamento-item/.ua-active-toggle/.ua-liderados-field) do formulário de edição de usuarios.html, mesmo o arquivo se chamando "perfis-acesso".