Reestruturação da tela de usuários
This commit is contained in:
parent
4f5c5a17f1
commit
131cf6ea85
40
CLAUDE.md
40
CLAUDE.md
@ -91,7 +91,7 @@ Um único app, `portal_api/`:
|
||||
| `permissions.py` | `PodeGerenciarPermissoes` — gate único de `gerencia_permissoes()` para as telas administrativas; `PermissaoApp(module_key, app_key)` — classe genérica reutilizável que checa `Usuario.permissao_app()`, instanciada por view (ex.: Links & Ferramentas e Ramais, ver seção própria abaixo). |
|
||||
| `views.py` | `login_view`/`logout_view`/`csrf_view` (auth por sessão), `me_view` (usuário + `permissoes_efetivas` já unidas no servidor + `lideranca`/`liderados`), `trocar_senha_view`, `usuarios_resumo_view`, `meus_liderados_view` (ver seção "Liderança"), `departamentos_resumo_view` (ver "Ramais"), `catalogo_view`, e os `ModelViewSet` de perfis/departamentos/usuários/compromissos/favoritos/widgets/notificações dispensadas/links e ferramentas/favoritos de links e ferramentas/seções e linhas de Acessos Gerais/ramais/ausências de ramal/importações de plano de saúde e suas linhas. |
|
||||
| `admin.py` | Django admin básico para todos os models (uso interno, não é a UI do portal). |
|
||||
| `management/commands/seed_portal.py` | Recria os 8 perfis padrão + `gabriel`/`bruno` + as 13 linhas de `FuncaoTelefonia` + o seed de `CategoriaEvento`. |
|
||||
| `management/commands/seed_portal.py` | Recria os 8 perfis padrão + `gabriel`/`bruno` + as 13 linhas de `FuncaoTelefonia` + o seed de `CategoriaEvento`; também realinha a sequence do Postgres por trás de `PerfilAcesso.codigo` (ver nota abaixo). |
|
||||
| `management/commands/seed_indicador_desempenho.py` | Popula o primeiro histórico do Indicador de Desempenho (7 `IndicadorCriterio` + 5 `IndicadorPercentualTipo`, idempotente) com os valores da planilha antiga — ver seção "Indicador de Desempenho" abaixo. |
|
||||
| `planos_saude/` | Pacote Python puro (sem ORM) com o pipeline de extração/casamento de "Importação de Plano de Saúde", portado de `projects/project/` — ver seção própria abaixo. |
|
||||
| `custo_contratacao/` | Pacote Python puro (sem ORM) da ferramenta "Simulação de Custo de Contratação" (Geradoc) — `tabelas.py` (seed/default das faixas de INSS/IRRF, hoje editáveis via `ParametroFiscalCustoContratacao`), `calculo.py` (`ParametrosFiscais` dataclass + `calcula_custo_empregado`), `pdf.py` (`gera_pdf_simulacao`, via `reportlab`). Ver seção própria abaixo. |
|
||||
@ -186,7 +186,7 @@ O item "Calendário De Paula" no menu **não é uma página local** — é um `<
|
||||
|
||||
## Ordem de `<script>` e por que ela não quebra nada
|
||||
|
||||
Todas as páginas-shell (todas exceto `index.html`) carregam o mesmo prefixo de scripts, na mesma ordem: `api.js → profiles.js → auth.js → access.js → account.js → favorites.js → events.js → ...`, seguido de scripts específicos da página. Nenhum script usa `defer` exceto `theme.js` (que fica no `<head>`).
|
||||
Todas as páginas-shell (todas exceto `index.html`) carregam o mesmo prefixo de scripts, na mesma ordem: `api.js → dual-select.js → profiles.js → auth.js → access.js → account.js → favorites.js → events.js → ...`, seguido de scripts específicos da página. Nenhum script usa `defer` exceto `theme.js` (que fica no `<head>`).
|
||||
|
||||
Isso importa porque, por exemplo, `access.js` chama `pidRequireAuth()` de `auth.js`, que só aparece **antes** dele na tag `<script>` — mas mesmo quando a ordem fosse invertida funcionaria, porque toda chamada cross-arquivo acontece dentro de um callback de `document.addEventListener("DOMContentLoaded", ...)`, nunca no nível superior do script. Como todo `<script>` sem `defer` executa (e portanto declara suas funções) antes do evento `DOMContentLoaded` disparar, a ordem relativa entre arquivos não importa — só importa que todos estejam presentes na página antes desse evento. Ao adicionar um novo arquivo JS compartilhado, não é preciso se preocupar em "colocá-lo antes de quem o usa", desde que toda chamada fique dentro de um handler de `DOMContentLoaded` (ou de uma função só invocada por um).
|
||||
|
||||
@ -250,6 +250,8 @@ Isso reaproveita 100% a árvore de permissões que já existe (`renderTree()`/`r
|
||||
|
||||
**Cuidado com `seed_portal.py`**: o módulo continua tendo seu próprio `enabled` (visibilidade no menu) e, se estiver em `BASE_KEYS`/`SECTORAL_KEYS` etc., `catalogo.permissions_from_keys()` habilita **todos** os apps de um módulo enabled de uma vez — incluindo todo `*-editar`. Por isso `seed_portal.py` força `permissoes["links-ferramentas"]["apps"]["links-ferramentas-editar"] = False`, `permissoes["links-ferramentas"]["apps"]["acessos-gerais-editar"] = False` e `permissoes["ramais"]["apps"]["ramais-editar"] = False` (+ as outras duas subtelas administráveis de Ramais) explicitamente pra todo perfil que não seja "Integração e Inovação" (código 8, `chaves=None` → todos os apps `True` de propósito), depois de gerar o dict — sem esse override, qualquer perfil com o módulo habilitado nasceria com poder de editar tudo. Ao adicionar uma aplicação nova nesse padrão (dentro de um módulo existente ou não), replicar esse mesmo cuidado no seed.
|
||||
|
||||
**Bug real (rodada 69) — `IntegrityError: duplicate key value violates unique constraint "portal_api_perfilacesso_pkey"` ao criar um perfil pela tela**: `seed_portal.py` semeia `PerfilAcesso` com `codigo` **explícito** (`update_or_create(codigo=dado["codigo"], ...)`, já que os 8 códigos 1–8 são referenciados por número fixo em vários lugares do código — ex.: `codigo == 8` = "Integração e Inovação"). No Postgres, um `INSERT` com PK explícita **nunca avança a sequence** por trás do `AutoField` — então a sequence ficava parada em 1 (seu valor inicial), e o primeiro perfil criado pela tela (`POST /api/perfis/`, sem PK explícita) recebia `codigo=1` do `nextval()`, colidindo com um código já usado pelo seed. `_reset_sequence(model)` (função módulo-level em `seed_portal.py`, roda `SELECT setval(pg_get_serial_sequence(...), MAX(pk))` via SQL puro do Postgres) corrige isso, chamada logo depois do loop de `PERFIS_SEED` — toda vez que `seed_portal` roda, a sequence é realinhada de novo. Se esse erro voltar a aparecer no futuro (ex.: um `loaddata`/`RunPython` de migração também inserindo `PerfilAcesso` com PK explícita sem passar por `seed_portal.py` depois), o comando pra corrigir manualmente é `python manage.py seed_portal` (idempotente, seguro rodar de novo) — não precisa de acesso direto ao banco.
|
||||
|
||||
### 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ó.
|
||||
@ -260,6 +262,16 @@ Botão `#pa-app-search-btn` ao lado de "Novo Perfil" (`perfis-acesso.html`) abre
|
||||
- **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=false` naquele perfil, o toggle também vira `perm.enabled = true` no mesmo PATCH — sem isso, o perfil ganharia a chave em `apps` mas o item continuaria escondido no menu (`access.js` esconde o `nav-group`/`nav-subitem` inteiro por `enabled`, 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" de `usuarios.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()` filtra `usuariosCacheAtual` por `is_active` antes de separar entre vinculado/disponível) — decisão explícita do usuário; na prática, inativar já limpa os `perfis` de 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 `--accent` quando ativo — mesmo padrão `.ua-sort-icon` já usado em `usuarios.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`) refaz `GET /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) dispara `bulkAlterarVinculo(kind, vincular)` — um `PATCH /api/usuarios/{id}/` (`pidSetUsuarioPerfis`) por usuário selecionado, em paralelo (`Promise.all`), cada um recalculando a própria lista de `perfis` (adiciona ou remove só o `codigo` do 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 por `openEdit()`) 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õe `email`) e `PATCH /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:
|
||||
@ -267,22 +279,30 @@ Usa o campo `is_active` que já vem de `AbstractUser` — não foi criado nenhum
|
||||
- **Login novo**: `authenticate()` (usado em `login_view`) roda via `django.contrib.auth.backends.ModelBackend`, que internamente chama `user_can_authenticate()` e recusa (`None`) qualquer usuário com `is_active=False`, mesmo com a senha certa. Como isso faz `authenticate()` retornar `None` tanto pra senha errada quanto pra usuário inativo, `login_view` faz 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 popular `request.user` a partir da sessão) também recusa usuários inativos, então na próxima requisição depois de desativado o usuário vira `AnonymousUser` automaticamente e `IsAuthenticated`/`PodeGerenciarPermissoes` já 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`) e do modal de Ramais (`ramais-lookup.js`), 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 tem `gerencia_permissoes`): seção "Liderança" no formulário de edição — checkbox "É gerente ou coordenador de outros usuários" (`#ua-lideranca`) libera (`hidden`) uma `.checklist-box` (`#ua-liderados-checklist`) com todos os outros usuários cadastrados (a própria conta sendo editada é excluída da lista — 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 envia `lideranca`+`liderados` (array de ids) no mesmo payload de `PATCH`/`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-modal` já existente, mesmo fluxo de antes) e, só se `me.lideranca` for `true`, uma segunda seção com a mesma `.checklist-box` de liderados — permitindo que o próprio gerente/coordenador se autogerencie sem precisar de acesso à tela administrativa de Usuários. Essa lista vem de `GET /api/usuarios-resumo/` (não de `/api/usuarios/`, que exige `gerencia_permissoes`) e salvar dispara `PATCH /api/me/liderados/`, que grava na mesma `Usuario.liderados` — `meus_liderados_view` recusa (403) se `request.user.lideranca` for `False`, já que só faz sentido pra quem tem o checkbox marcado.
|
||||
- **Tela de Usuários** (`usuarios.html`/`users-admin.js`, só quem tem `gerencia_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 envia `lideranca`+`liderados` (array de ids) no mesmo payload de `PATCH`/`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-modal` já existente, mesmo fluxo de antes) e, só se `me.lideranca` for `true`, 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 de `GET /api/usuarios-resumo/` (não de `/api/usuarios/`, que exige `gerencia_permissoes`) e salvar dispara `PATCH /api/me/liderados/`, que grava na mesma `Usuario.liderados` — `meus_liderados_view` recusa (403) se `request.user.lideranca` for `False`, já que só faz sentido pra quem tem o checkbox marcado.
|
||||
|
||||
**Busca por nome/departamento e layout em duas linhas**: `UsuarioResumoSerializer` (usado por `/api/usuarios-resumo/` e por `liderados` aninhado em `UsuarioListSerializer`/`/api/me/`) expõe também `departamentos` (nested `DepartamentoSerializer`), não só `id`/`nome`. Os dois pontos que renderizam o checklist de liderados (`users-admin.js`/`account.js`) usam isso pra: (a) cada item (`.checklist-item`) mostrar o nome e, numa segunda linha abaixo (`.checklist-item__info` empilha os dois em coluna), os departamentos do candidato unidos por vírgula (`—` se nenhum) — layout em duas linhas com quebra normal (não uma só linha com `text-overflow: ellipsis`) porque truncar cortava departamentos no meio quando o usuário tinha mais de um vinculado; (b) um campo de busca (`.checklist-search`, `#ua-liderados-search`/`#manage-account-liderados-search`) que filtra a lista pelo nome do usuário **ou** pelo texto dos departamentos (`String.includes`, case-insensitive) — qualquer um dos dois casa. A seleção de checkboxes é mantida em memória num `Set` (`lideradosSelecionados`, um por tela) em vez de lida direto do DOM, porque filtrar re-renderiza `innerHTML` e apagaria o estado de quem estivesse marcado mas momentaneamente fora do filtro; um listener de `change` delegado no container (`.checklist-box`) atualiza esse `Set` a cada clique, e o array final enviado à API vem de `Array.from(set)`, não de uma querystring nos checkboxes visíveis.
|
||||
|
||||
**"Marcar todos os resultados da busca"** (`.checklist-select-all`, `#ua-liderados-select-all`/`#manage-account-liderados-select-all`): opera só sobre o subconjunto **filtrado** pela busca no momento (não sobre todos os candidatos) — marcar adiciona o id de cada usuário filtrado ao `Set`, desmarcar remove; a cada re-render, o próprio estado desse checkbox é recalculado a partir do filtro atual (`checked` se todos os filtrados estão no `Set`, `indeterminate` se só parte, `disabled` se a busca não retornou ninguém), então ele reflete o resultado visível, não um total geral fixo.
|
||||
|
||||
`.checklist-box`/`.checklist-item` (+ `.checklist-item__nome`/`.checklist-item__departamento`/`.checklist-empty`/`.checklist-search`, `components.css`) é o componente genérico de "lista com checkbox, coluna secundária e busca" reaproveitado pelos três checklists de `usuarios.html` (Perfis de Acesso, Departamento, Liderados — os dois primeiros sem coluna/busca, só o layout base) **e** pelo checklist de liderados do modal "Gerenciar Usuário" — por isso mora em `components.css` (carregado em todo shell), não em `perfis-acesso.css` (só carregado em `usuarios.html`/`perfis-acesso.html`/`ramais.html`), onde vivia antes de existir um segundo consumidor fora dessas três páginas.
|
||||
**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 seção própria abaixo), 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-card` do modal "Gerenciar Usuário") ganha a classe `.modal-card--wide` via JS (`account.js`) só quando `me.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 em `components.css` (não em `perfis-acesso.css`), pela mesma razão de `.checklist-box` — o modal "Gerenciar Usuário" existe em todo shell, e a maioria deles não carrega `perfis-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`) filtra `users` por `is_active` antes de montar a lista de candidatos; no modal "Gerenciar Usuário", isso já vem de graça porque `GET /api/usuarios-resumo/` (`usuarios_resumo_view`) só devolve usuários ativos.
|
||||
|
||||
## Favoritos: como o ID de uma aplicação é derivado
|
||||
|
||||
@ -594,7 +614,7 @@ Terceiro checkbox de "Tipo de importação" (ao lado de Mensalidade/Coparticipa
|
||||
| `tokens.css` | Variáveis (`:root`, tema claro em `:root[data-theme="light"]`). |
|
||||
| `base.css` | Reset global, incluindo `[hidden] { display: none !important; }` — necessário porque vários componentes (`.no-access`, `.app-card`, `.notif-badge`) definem seu próprio `display`, o que sem o `!important` sobrescreveria o comportamento nativo de `hidden`. Também os `@keyframes` globais de animação (`pidFadeIn`/`pidFadeSlideUp`/`pidScaleIn`, ver "Animações" abaixo), já que é o único CSS carregado por **todas** as páginas sem exceção (inclusive `index.html`). |
|
||||
| `layout.css` | Casca do shell: `.app-shell`, `.sidebar*`, `.nav-*`, `.fav-toggle`, `.topbar*`. A sidebar usa tokens **congelados**, independentes de tema (fundo sempre escuro em claro/escuro) — não trocar por variáveis que espelham `:root[data-theme="light"]`. Exceção deliberada: `--sidebar-text-primary`/`--sidebar-text-secondary`/`--sidebar-text-muted` (texto/ícone do menu) *são* sobrescritas em `:root[data-theme="light"]` (`tokens.css`) pra branco puro — pedido explícito do usuário pra melhorar a legibilidade; só o fundo/borda da sidebar continuam frozen. Também `.page-content` (largura do conteúdo de cada página, `max-width:1200px` centralizado por padrão) + o modificador `.page-content--wide` (`max-width:1600px`) — `portal.html` ("Principal") é a única página que usa só `.page-content` puro (grade de favoritos fica mais confortável de leitura mais estreita); as outras 7 páginas (`perfis-acesso.html`, `usuarios.html`, `links-ferramentas.html`, `acessos-gerais.html`, `ramais.html`, `calendario-individual.html`, `importacao-plano-saude.html`) usam `class="page-content page-content--wide"` no `<main>`, decisão explícita do usuário pra aproveitar melhor o espaço entre a sidebar e a borda da tela em telas de tabela/formulário. Uma página nova que seja mais "aplicação" (tabela, formulário, CRUD) do que "dashboard" deve nascer já com `page-content--wide`. |
|
||||
| `components.css` | UI genérica reutilizável: `.btn-solid/.btn-outline/.btn-ghost/.btn-danger-outline`, **`.modal-overlay`/`.modal-card`** (moldura genérica de modal) **e também** `.modal-field`/`.modal-field-row`/`.modal-checkbox`/`.modal-error`/`.modal-actions` (campos internos do modal — moldura e campos vivem juntos aqui, apesar do nome sugerir só a moldura, incluindo `select`/`textarea` dentro de `.modal-field`, com seta customizada via `background-image` porque o nativo do browser destoa do tema escuro), `.app-card*`, `.no-access`, `.checklist-box`/`.checklist-item`/`.checklist-item__info`/`.checklist-item__nome`/`.checklist-item__departamento`/`.checklist-empty`/`.checklist-search`/`.checklist-select-all` (lista com checkbox, segunda linha de detalhe e busca — usada em `usuarios.html` e no modal "Gerenciar Usuário" de todo shell, ver seção "Liderança"). |
|
||||
| `components.css` | UI genérica reutilizável: `.btn-solid/.btn-outline/.btn-ghost/.btn-danger-outline`, **`.modal-overlay`/`.modal-card`** (moldura genérica de modal, + o modificador `.modal-card--wide` pra quando precisa de mais espaço horizontal) **e também** `.modal-field`/`.modal-field-row`/`.modal-checkbox`/`.modal-error`/`.modal-actions` (campos internos do modal — moldura e campos vivem juntos aqui, apesar do nome sugerir só a moldura, incluindo `select`/`textarea` dentro de `.modal-field`, com seta customizada via `background-image` porque o nativo do browser destoa do tema escuro), `.app-card*`, `.no-access`, `.checklist-box`/`.checklist-item`/`.checklist-item__info`/`.checklist-item__nome`/`.checklist-item__departamento`/`.checklist-empty`/`.checklist-search`/`.checklist-select-all` (lista com checkbox, segunda linha de detalhe e busca — usada nos checklists de Perfis de Acesso/Departamento em `usuarios.html`) e `.dual-select`/`.dual-select__*` (vinculação em duas tabelas — não vinculados/vinculados, ver seção "Liderança" — usada em `usuarios.html` e no modal "Gerenciar Usuário" de todo shell). |
|
||||
| `perfis-acesso.css` | `.pa-*` (tela de Perfis de Acesso), incluindo 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`. |
|
||||
| `calendario.css` | Só `.calendar-*` (grade mensal, células de dia, nav do mês) — os campos do modal de compromisso usam as classes genéricas `.modal-field`/`.modal-checkbox`/`.modal-error`/`.modal-actions` de `components.css`. |
|
||||
| `widgets.css` | `.widgets-*`, `.widget-card*`, `.widget-picker-*` — só usado em `portal.html`; também tem `#portal-title` (fonte "Bree Serif" do título "Portal De Paula" no topbar), que não é widget mas mora aqui por ser o único CSS próprio da página. |
|
||||
|
||||
54
plano.md
54
plano.md
@ -924,6 +924,60 @@ Validado rodando o parser (`BradescoSaude.extrai`) e o pipeline completo (`proce
|
||||
|
||||
Registrado em `pipeline.OPERADORAS` como `"bradesco_saude": {"label": "1386 - Bradesco Saúde", ...}`.
|
||||
|
||||
### 65. Filtro Ativos/Inativos/Todos na lista de Usuários
|
||||
|
||||
Usuário pediu um botão pra filtrar a lista de `usuarios.html` entre Ativos/Inativos, abrindo por padrão só com os Ativos, mas com opção de tirar o filtro pra ver os inativos também.
|
||||
|
||||
Implementado como 3 chips ("Ativos"/"Inativos"/"Todos", `#ua-status-filtros`, ao lado do título "Usuários") — mesma linguagem visual de `.ind-departamento-chip` (Indicador de Desempenho). Filtro 100% client-side sobre o array `users` já carregado (`statusFiltro`, padrão `"ativos"`), aplicado em `renderList()` antes da busca por texto. Sem endpoint novo.
|
||||
|
||||
### 66. Colunas de código cadastral (Folha/Questor/Tareffa/Contabit/Ramal) + ordenação na lista de Usuários
|
||||
|
||||
Usuário pediu pra ver os campos cadastrais opcionais (`codigo_folha`/`codigo_questor`/`codigo_tareffa`/`codigo_contabit`/`ramal`, hoje só visíveis dentro do formulário de edição) como colunas na lista, e poder ordenar por elas — mesmo sendo campos opcionais, o objetivo é achar quem está sem algum deles cadastrado antes de outras aplicações passarem a depender dessas informações.
|
||||
|
||||
`UsuarioListSerializer` já expunha os 5 campos (nenhuma mudança de backend necessária). Adicionadas 5 colunas em `#ua-table` (`usuarios.html`) entre Nome e Perfil de Acesso; célula vazia mostra `—` em itálico apagado (`.ua-campo-vazio`) em vez de ficar em branco, pra ficar visualmente óbvio ao ordenar. Cabeçalhos com `data-sort` (login, nome, os 4 códigos, ramal, status) ficam clicáveis e alternam asc/desc — mesmo padrão (`comparaValoresUsuario`, ícone `↕` que vira `--accent` no ativo) já usado em `#ips-list-table` (Importação de Plano de Saúde) e no modal de Ramais.
|
||||
|
||||
### 67. Aba "Usuários do Escritório" (Perfis de Acesso) reestruturada em duas tabelas com seleção múltipla
|
||||
|
||||
Usuário anexou um print de outro sistema (duas grades lado a lado, cada uma com checkbox, busca por coluna, ordenação e um botão de ação em lote — "VINCULAR"/"DESVINCULAR") e pediu pra reestruturar a aba "Usuários do Escritório" (dentro da edição de um perfil, `perfis-acesso.html`) nesse mesmo estilo: ver todos os usuários ativos de um lado e vincular, ver os já vinculados do outro lado e desvincular.
|
||||
|
||||
Substituído o antigo `<select>` + botão "Vincular" + lista simples com X (`.pa-users-add`/`.pa-user-list`, removidos) por `.pa-users-dual` — duas tabelas (`#pa-users-available-*` à esquerda, só usuários ativos ainda não vinculados; `#pa-users-linked-*` à direita, todos os já vinculados, inclusive inativos, com selo `.status-pill--inativo`), cada uma com checkbox por linha + "selecionar todos" (sobre as linhas filtradas visíveis), busca por nome/e-mail, ordenação por nome (clique no cabeçalho, ícone `↕`) e um botão de atualizar. "Vincular"/"Desvincular" operam em lote sobre a seleção (`bulkAlterarVinculo()`, `Promise.all` de `PATCH /api/usuarios/{id}/` por usuário selecionado) — sem endpoint novo, só reaproveitando `GET/PATCH /api/usuarios/` que já existiam. Abrir a aba de um perfil diferente sempre reseta seleção/ordenação/busca dos dois painéis.
|
||||
|
||||
### 68. Botão "Vincular" (só visual) nos campos Código da Folha/Questor/Tareffa
|
||||
|
||||
Usuário anexou um print mostrando 3 campos específicos (Código da Folha, Código do Questor, Código do Tareffa — não Contabit nem Ramal) e explicou que, no futuro, esses códigos serão buscados/vinculados automaticamente a partir de ferramentas externas (ex.: `codigo_tareffa` via a view que já existe no banco pra ler o Tareffa) em vez de digitados manualmente. Pediu explicitamente pra **não** construir essa vinculação agora — só deixar a estrutura visual pronta, com um botão "Vincular".
|
||||
|
||||
Passou por 3 versões visuais no mesmo dia até o formato final: (1) botão "Vincular" solto ao lado do input (`.btn-outline`, com texto); (2) só o ícone de elo no canto do próprio input (`position:absolute`, sem texto — pedido do usuário: "deixe o botão mais simples, só o símbolo no canto"); (3) formato final — input + botão "Vincular" (ícone + texto) encostados numa única caixa (`.ua-field-link`, borda/raio únicos, `border-left` separando os dois), depois do usuário mostrar um print de referência (campo de busca com botão "Buscar" atado à direita) e pedir esse mesmo estilo. Sempre `disabled` com `title` explicando que a vinculação ainda não foi implementada. Sem handler de JS nenhum — puramente estrutural, pra reaproveitar quando a integração de verdade for construída.
|
||||
|
||||
### 69. Bug: `IntegrityError` ao criar um novo perfil de acesso (`codigo` duplicado)
|
||||
|
||||
Usuário reportou erro `500` ao criar um perfil pela tela: `django.db.utils.IntegrityError: duplicate key value violates unique constraint "portal_api_perfilacesso_pkey" — DETAIL: Key (codigo)=(6) already exists`.
|
||||
|
||||
Causa: `seed_portal.py` semeia os 8 perfis com `codigo` **explícito** (`update_or_create(codigo=dado["codigo"], ...)`, necessário porque vários lugares do código referenciam código fixo, ex. `codigo == 8`). No Postgres, `INSERT` com PK explícita não avança a sequence do `AutoField` — a sequence de `PerfilAcesso.codigo` ficou parada em 1 desde a criação do banco, então o primeiro `POST /api/perfis/` (sem PK explícita) pediu `nextval()` e recebeu um código baixo já usado pelo seed (6 = "Financeiro").
|
||||
|
||||
Corrigido com `_reset_sequence()` (nova função em `seed_portal.py`, `SELECT setval(pg_get_serial_sequence(...), MAX(codigo))` via SQL puro) chamada logo após semear `PERFIS_SEED` — toda execução do comando realinha a sequence de novo, então o bug não volta a acontecer mesmo que o seed seja reexecutado no futuro. Aplicado no banco em produção rodando `python manage.py seed_portal` (confirmado via `SELECT last_value FROM portal_api_perfilacesso_codigo_seq` = 8, igual ao maior código existente).
|
||||
|
||||
### 70. "Usuários sob liderança" reestruturado em duas tabelas (não vinculados/vinculados)
|
||||
|
||||
Usuário anexou dois prints — um da própria seção "Liderança" (checklist único, com busca e "marcar todos") e outro de um sistema externo com duas grades lado a lado (checkbox, "Código"/"Nome", ordenação, paginação, VINCULAR/DESVINCULAR) — e pediu pra reestruturar "Usuários sob liderança" nesse mesmo estilo de duas tabelas: não vinculados de um lado, vinculados do outro (corrigido depois pra confirmar: não vinculados à **esquerda**, vinculados à **direita** — mesma convenção já usada no widget "Usuários do Escritório" de Perfis de Acesso, rodada 67).
|
||||
|
||||
Essa seção aparece em dois lugares (formulário de edição de usuário em `usuarios.html` **e** o modal "Gerenciar Usuário", presente em todo shell, pra autogestão do próprio gerente/coordenador) — perguntado ao usuário se a mudança deveria valer só pra tela de Usuários ou pros dois lugares; resposta: os dois.
|
||||
|
||||
Como o widget "Usuários do Escritório" (rodada 67) já tinha esse exato padrão visual mas vivia só em `perfis-acesso.css`/inline em `profiles.js` (só usado nessa página), foi extraída uma versão genérica reaproveitável: `static/js/dual-select.js` (`pidCriarSeletorDuplo()`, fábrica que recebe as referências de DOM de cada painel + uma função pro valor da coluna secundária, devolve `{setDados, getVinculadosIds}` — vinculação só em memória, sem chamada de API própria) + `.dual-select`/`.dual-select__*` em `components.css` (não em `perfis-acesso.css`, já que o modal "Gerenciar Usuário" existe em todo shell e a maioria não carrega esse CSS). `dual-select.js` foi adicionado ao prefixo de scripts compartilhado das 10 páginas-shell, logo depois de `api.js`.
|
||||
|
||||
Trocados: `usuarios.html` (seção "Liderança") e as 10 cópias do modal "Gerenciar Usuário" (uma por shell) — `.checklist-box`+busca+"marcar todos" virou o par de tabelas com coluna "Nome" (ordenável) + "Departamento" (busca própria). `#manage-account-modal-card` ganhou id novo pra `account.js` poder aplicar `.modal-card--wide` só quando `me.lideranca` é `true` (o modal padrão de 420px não cabe duas tabelas lado a lado). `users-admin.js`/`account.js` tiveram a lógica de checklist único substituída por chamadas a `pidCriarSeletorDuplo` — o restante do fluxo (salvar `liderados` junto do form em `usuarios.html`, `PATCH /api/me/liderados/` imediato no modal) não mudou.
|
||||
|
||||
**Ajuste de proporção no mesmo dia**: usuário reportou dois problemas visuais depois de testar — (1) "Perfis de Acesso" e "Departamento" (`usuarios.html`) não ficaram do mesmo tamanho; (2) o par de tabelas de "Usuários sob liderança" ficou comprimido num canto, sem usar a largura da seção. Causa de (1): `.pa-field-row.ua-permissions-row` usava `grid-template-columns: 1.6fr 1fr` (resquício de quando só existia um checklist de cada lado, sem se importar com simetria) — trocado para `1fr 1fr`. Causa de (2): `.ua-liderados-field` tinha `max-width: 480px`, herdado de quando o campo continha só um `.checklist-box` estreito — removido, já que agora o campo precisa da largura inteira da seção pras duas tabelas do `.dual-select`.
|
||||
|
||||
### 72. Inativar usuário desvincula perfis de acesso e liderança automaticamente
|
||||
|
||||
Regra de negócio pedida pelo usuário: ao marcar um usuário como Inativo, ele deve ser desvinculado automaticamente de todos os perfis de acesso que tinha, e removido da lista de liderados de qualquer gerente que o tivesse — pra não deixar um vínculo "pendurado" em alguém que já não pode mais acessar o Portal.
|
||||
|
||||
Implementado em `_revogar_acesso_se_inativo()` (`serializers.py`), chamada ao final de `UsuarioSerializer.create()`/`update()` (depois dos `.set()` de `perfis`/`departamentos`/`liderados`, nunca antes — o formulário de edição sempre reenvia o checklist de perfis inteiro junto com qualquer alteração no checkbox "Usuário ativo", então limpar antes dos `.set()` seria desfeito na mesma requisição). Cobre os dois pontos reais de entrada (botão de alternar na lista e checkbox no formulário), ambos via `PATCH /api/usuarios/{id}/`.
|
||||
|
||||
Uma primeira versão tentou resolver isso em `Usuario.save()` (model), mas foi abandonada: tanto o `ModelSerializer.update()` do DRF quanto o admin do Django chamam `.set()`/`save_m2m()` nos M2M **depois** de `instance.save()`, então uma limpeza dentro de `save()` seria sempre desfeita pelo `.set(perfis)` que vem a seguir no mesmo request. Por isso a limpeza mora no serializer, como o último passo, não no model.
|
||||
|
||||
**Complemento no mesmo dia**: usuário reportou visualmente que usuários inativos ainda apareciam nos dois widgets de vinculação (dual-select de "Usuários sob liderança" e de "Usuários do Escritório" em Perfis de Acesso) — pediu que usuário inativo nunca seja exibido em nenhum dos dois painéis (nem disponível, nem já vinculado). Corrigido: `listaParaPainelUsuarios()` (`profiles.js`) agora filtra `usuariosCacheAtual` por `is_active` antes de separar entre disponível/vinculado (removido também o selo "Inativo" que antes marcava um usuário inativo ainda vinculado, já que esse caso não deve mais aparecer); `fillLideradosChecklist()` (`users-admin.js`) ganhou o mesmo filtro. No modal "Gerenciar Usuário" (`account.js`) já não era um problema — `GET /api/usuarios-resumo/` só devolve usuários ativos.
|
||||
|
||||
## Roadmap / próximos passos
|
||||
|
||||
Nenhuma pendência explícita em aberto no momento, exceto a limitação conhecida
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
from typing import Any
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db import transaction
|
||||
from django.db import connection, transaction
|
||||
from django.db.models import Model
|
||||
|
||||
from portal_api import catalogo
|
||||
from portal_api.models import CategoriaEvento, FuncaoTelefonia, PerfilAcesso, Usuario
|
||||
@ -115,6 +116,27 @@ PERFIS_SEED = [
|
||||
]
|
||||
|
||||
|
||||
def _reset_sequence(model: type[Model]) -> None:
|
||||
"""Realinha a sequence do Postgres por trás do AutoField de `model` com o
|
||||
maior valor de PK já usado na tabela.
|
||||
|
||||
Necessário porque `PerfilAcesso` é populado abaixo com `codigo` explícito
|
||||
(`update_or_create(codigo=dado["codigo"], ...)`) — um INSERT com PK
|
||||
explícita no Postgres nunca avança a sequence associada ao `serial`, então
|
||||
o próximo INSERT sem PK explícita (criar um perfil novo pela tela) volta a
|
||||
gerar códigos baixos (1, 2, ...) que colidem com os perfis já seedados,
|
||||
estourando `IntegrityError: duplicate key value violates unique
|
||||
constraint`. Reexecutar o seed sempre corrige isso de novo.
|
||||
"""
|
||||
table = model._meta.db_table
|
||||
pk_column = model._meta.pk.column
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
f"SELECT setval(pg_get_serial_sequence('{table}', '{pk_column}'), "
|
||||
f"COALESCE((SELECT MAX({pk_column}) FROM {table}), 1))"
|
||||
)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Recria os 8 perfis de acesso padrão e os usuários de demonstração (gabriel/bruno)."
|
||||
|
||||
@ -156,6 +178,8 @@ class Command(BaseCommand):
|
||||
acao = "criado" if criado else "atualizado"
|
||||
self.stdout.write(f"Perfil {acao}: {perfil.nome} (código {perfil.codigo})")
|
||||
|
||||
_reset_sequence(PerfilAcesso)
|
||||
|
||||
integracao = PerfilAcesso.objects.get(codigo=8)
|
||||
|
||||
# Só inicializa nome/senha/perfis na primeira criação — reexecuções não devem
|
||||
|
||||
@ -99,6 +99,24 @@ class UsuarioResumoSerializer(serializers.ModelSerializer):
|
||||
fields = ["id", "nome", "departamentos"]
|
||||
|
||||
|
||||
def _revogar_acesso_se_inativo(usuario: Usuario) -> None:
|
||||
"""Decisão explícita do usuário: uma conta inativa (`is_active=False`) não
|
||||
deve continuar vinculada a nenhum perfil de acesso, nem aparecer sob a
|
||||
liderança de nenhum gerente — sem isso, o vínculo ficaria "pendurado" em
|
||||
alguém que já não pode mais logar. Chamado sempre **depois** dos
|
||||
`.set()` de `perfis`/`liderados` em `create()`/`update()`, pra não ser
|
||||
imediatamente desfeito se o mesmo payload também enviar `perfis` (ex.: o
|
||||
formulário de edição de `usuarios.html` sempre reenvia o checklist
|
||||
inteiro, mesmo quando só o checkbox "Usuário ativo" mudou). `lideres` é a
|
||||
relação reversa de `Usuario.liderados` — limpar aqui remove `usuario` do
|
||||
`liderados` de todo gerente que o tivesse, sem afetar quem `usuario`
|
||||
eventualmente lidera."""
|
||||
if usuario.is_active:
|
||||
return
|
||||
usuario.perfis.clear()
|
||||
usuario.lideres.clear()
|
||||
|
||||
|
||||
class UsuarioSerializer(serializers.ModelSerializer):
|
||||
"""Usado para criar/editar (aceita `senha` e `perfis` graváveis)."""
|
||||
|
||||
@ -143,6 +161,7 @@ class UsuarioSerializer(serializers.ModelSerializer):
|
||||
usuario.perfis.set(perfis)
|
||||
usuario.departamentos.set(departamentos)
|
||||
usuario.liderados.set(liderados)
|
||||
_revogar_acesso_se_inativo(usuario)
|
||||
return usuario
|
||||
|
||||
def update(self, instance: Usuario, validated_data: dict[str, Any]) -> Usuario:
|
||||
@ -161,6 +180,7 @@ class UsuarioSerializer(serializers.ModelSerializer):
|
||||
instance.departamentos.set(departamentos)
|
||||
if liderados is not None:
|
||||
instance.liderados.set(liderados)
|
||||
_revogar_acesso_se_inativo(instance)
|
||||
return instance
|
||||
|
||||
|
||||
|
||||
@ -127,6 +127,15 @@
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Modal que precisa de mais espaço horizontal que o padrão de 420px — hoje
|
||||
usado só por #manage-account-modal quando o campo de liderados (duas
|
||||
tabelas lado a lado, ver .dual-select abaixo) está visível; aplicado/
|
||||
removido em JS conforme `me.lideranca`, pra não deixar o modal largo à
|
||||
toa quando só tem o botão "Alterar senha". */
|
||||
.modal-card--wide {
|
||||
max-width: 760px;
|
||||
}
|
||||
|
||||
/* Modal que embute uma ferramenta externa num <iframe> (ex.: "Calendário De
|
||||
Paula" no sidebar, todo shell) — ocupa quase a tela inteira e o iframe
|
||||
preenche todo o espaço vertical restante (flex:1 dentro do .modal-card,
|
||||
@ -365,6 +374,146 @@
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
/* Widget genérico de vinculação em duas tabelas (não vinculados à esquerda,
|
||||
vinculados à direita — ver static/js/dual-select.js), com checkbox de
|
||||
seleção múltipla, busca por coluna, ordenação por nome e um botão de ação
|
||||
em lote por painel ("Vincular"/"Desvincular"). Mora aqui, não em
|
||||
perfis-acesso.css, porque roda tanto em usuarios.html quanto dentro do
|
||||
modal "Gerenciar Usuário" (presente em todo shell) — mesma razão de
|
||||
.checklist-box viver aqui. Auto-contido (não reaproveita .pa-table),
|
||||
porque a maioria dos shells que usam esse modal não carrega
|
||||
perfis-acesso.css. */
|
||||
.dual-select {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--space-4);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.dual-select {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.dual-select__panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.dual-select__panel-header .btn-solid:disabled,
|
||||
.dual-select__panel-header .btn-danger-outline:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.dual-select__table-wrap {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
max-height: 280px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.dual-select__table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.dual-select__table thead {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.dual-select__table th {
|
||||
text-align: left;
|
||||
font-size: 0.72rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--text-muted);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
background: var(--bg-surface-raised);
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.dual-select__table td {
|
||||
padding: var(--space-2) var(--space-3);
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.dual-select__table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.dual-select__table tr:hover td {
|
||||
background: var(--card-bg-hover);
|
||||
}
|
||||
|
||||
.dual-select__table th input[type="checkbox"],
|
||||
.dual-select__table td input[type="checkbox"] {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
accent-color: var(--accent);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.dual-select__sort {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dual-select__sort-icon {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
|
||||
.dual-select__sort.is-sort-asc .dual-select__sort-icon,
|
||||
.dual-select__sort.is-sort-desc .dual-select__sort-icon {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.dual-select__filters th {
|
||||
background: var(--bg-surface);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
}
|
||||
|
||||
.dual-select__filters input {
|
||||
width: 100%;
|
||||
padding: var(--space-1) var(--space-2);
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border-subtle);
|
||||
background: var(--bg-canvas);
|
||||
color: var(--text-primary);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 400;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.dual-select__filters input:focus {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.dual-select__empty-row td {
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
padding: var(--space-5);
|
||||
}
|
||||
|
||||
.dual-select__footer {
|
||||
padding: var(--space-2) var(--space-1) 0;
|
||||
font-size: 0.76rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.btn-ghost:hover {
|
||||
background: var(--accent);
|
||||
color: var(--accent-contrast);
|
||||
|
||||
@ -48,6 +48,33 @@
|
||||
color: var(--accent-contrast);
|
||||
}
|
||||
|
||||
/* Ordenação por coluna na tabela de usuários (#ua-table) — mesma linguagem
|
||||
visual do ícone ↕ estático já usada em #ips-list-table
|
||||
(importacao-plano-saude.css) e no modal de Ramais (ramais-lookup.css). */
|
||||
#ua-table th[data-sort] {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ua-sort-icon {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
#ua-table th[data-sort].is-sort-asc .ua-sort-icon,
|
||||
#ua-table th[data-sort].is-sort-desc .ua-sort-icon {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* Marca visualmente um código cadastral (folha/Questor/Tareffa/Contabit)
|
||||
ou ramal não preenchido — ordenar a coluna agrupa essas linhas, para
|
||||
achar quem ainda está sem cadastro. */
|
||||
.ua-campo-vazio {
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.pa-search {
|
||||
position: relative;
|
||||
width: 260px;
|
||||
@ -283,7 +310,7 @@
|
||||
}
|
||||
|
||||
.pa-field-row.ua-permissions-row {
|
||||
grid-template-columns: 1.6fr 1fr;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
@ -384,9 +411,60 @@
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Campo cadastral com vinculação externa futura (Código da Folha/Questor/
|
||||
Tareffa) — input + botão "Vincular" formando uma única caixa (mesmo
|
||||
estilo de referência mostrado pelo usuário: campo de busca com o botão
|
||||
"Buscar" encostado à direita, separado por uma linha divisória);
|
||||
desabilitado de propósito, já que a busca/vínculo de verdade com a
|
||||
ferramenta externa (ex.: view do Tareffa no banco) ainda não foi
|
||||
implementada. */
|
||||
.ua-field-link {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border-subtle);
|
||||
overflow: hidden;
|
||||
background: var(--bg-canvas);
|
||||
}
|
||||
|
||||
.ua-field-link:focus-within {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.ua-field-link input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
border: none !important;
|
||||
border-radius: 0 !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.ua-vincular-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
flex-shrink: 0;
|
||||
padding: 0 var(--space-3);
|
||||
border-left: 1px solid var(--border-subtle);
|
||||
background: var(--bg-surface-raised);
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.82rem;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ua-vincular-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.ua-vincular-btn:not(:disabled):hover {
|
||||
color: var(--accent);
|
||||
background: var(--bg-surface);
|
||||
}
|
||||
|
||||
.ua-liderados-field {
|
||||
margin-top: var(--space-3);
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.ua-departamento-item {
|
||||
@ -637,62 +715,97 @@
|
||||
accent-color: var(--accent);
|
||||
}
|
||||
|
||||
/* Usuários do escritório */
|
||||
.pa-users-add {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
margin-bottom: var(--space-4);
|
||||
max-width: 420px;
|
||||
/* Usuários do escritório — duas tabelas lado a lado (disponíveis pra
|
||||
vincular / já vinculados a este perfil), cada uma com checkbox de
|
||||
seleção múltipla, busca por coluna e ordenação por nome; "Vincular"/
|
||||
"Desvincular" operam em lote sobre a seleção da tabela correspondente. */
|
||||
.pa-users-dual {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--space-5);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.pa-users-add select {
|
||||
flex: 1;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
@media (max-width: 900px) {
|
||||
.pa-users-dual {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.pa-users-panel__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: var(--space-2);
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
.pa-users-table__checkbox {
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.pa-users-table th input[type="checkbox"] {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
accent-color: var(--accent);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.pa-users-table td input[type="checkbox"] {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
accent-color: var(--accent);
|
||||
}
|
||||
|
||||
.pa-users-table__sort {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pa-users-table__sort.is-sort-asc .ua-sort-icon,
|
||||
.pa-users-table__sort.is-sort-desc .ua-sort-icon {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.pa-users-table__filters th {
|
||||
background: var(--bg-surface);
|
||||
padding: var(--space-2) var(--space-4);
|
||||
}
|
||||
|
||||
.pa-users-table__filters input {
|
||||
width: 100%;
|
||||
padding: var(--space-1) var(--space-2);
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border-subtle);
|
||||
background: var(--bg-canvas);
|
||||
color: var(--text-primary);
|
||||
font-size: 0.88rem;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 400;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.pa-users-add select:focus {
|
||||
.pa-users-table__filters input:focus {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.pa-user-login {
|
||||
color: var(--text-muted);
|
||||
font-weight: 400;
|
||||
.pa-users-panel .pa-table-wrap {
|
||||
max-height: 360px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.pa-users-empty {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.85rem;
|
||||
.pa-users-panel .pa-table thead {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.pa-user-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
.pa-user-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-surface-raised);
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.pa-user-remove {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.pa-user-remove:hover {
|
||||
color: var(--danger);
|
||||
.pa-users-panel .btn-solid:disabled,
|
||||
.pa-users-panel .btn-danger-outline:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.pa-edit-actions {
|
||||
|
||||
@ -59,73 +59,38 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
});
|
||||
|
||||
const manageAccountModal = document.getElementById("manage-account-modal");
|
||||
const manageAccountModalCard = document.getElementById("manage-account-modal-card");
|
||||
const manageAccountChangePasswordBtn = document.getElementById("manage-account-change-password-btn");
|
||||
const manageAccountCloseBtn = document.getElementById("manage-account-close-btn");
|
||||
const manageAccountLideradosField = document.getElementById("manage-account-liderados-field");
|
||||
const manageAccountLideradosSearch = document.getElementById("manage-account-liderados-search");
|
||||
const manageAccountLideradosSelectAll = document.getElementById("manage-account-liderados-select-all");
|
||||
const manageAccountLideradosChecklist = document.getElementById("manage-account-liderados-checklist");
|
||||
const manageAccountLideradosError = document.getElementById("manage-account-liderados-error");
|
||||
const manageAccountSaveBtn = document.getElementById("manage-account-save-btn");
|
||||
|
||||
if (manageAccountBtn && manageAccountModal) {
|
||||
let lideradosCandidatos = [];
|
||||
let lideradosSelecionados = new Set();
|
||||
|
||||
function departamentosTexto(usuario) {
|
||||
return (usuario.departamentos || []).map((d) => d.nome).join(", ");
|
||||
}
|
||||
|
||||
function lideradosFiltrados() {
|
||||
const termo = manageAccountLideradosSearch.value.trim().toLowerCase();
|
||||
return lideradosCandidatos.filter(
|
||||
(u) => !termo || u.nome.toLowerCase().includes(termo) || departamentosTexto(u).toLowerCase().includes(termo)
|
||||
);
|
||||
}
|
||||
|
||||
function renderManageAccountLiderados() {
|
||||
const filtrados = lideradosFiltrados();
|
||||
manageAccountLideradosChecklist.innerHTML = filtrados.length
|
||||
? filtrados
|
||||
.map(
|
||||
(u) => `
|
||||
<label class="checklist-item">
|
||||
<input type="checkbox" value="${u.id}" ${lideradosSelecionados.has(u.id) ? "checked" : ""} />
|
||||
<span class="checklist-item__info">
|
||||
<span class="checklist-item__nome">${u.nome}</span>
|
||||
<span class="checklist-item__departamento">${departamentosTexto(u) || "—"}</span>
|
||||
</span>
|
||||
</label>`
|
||||
)
|
||||
.join("")
|
||||
: `<p class="checklist-empty">Nenhum usuário encontrado.</p>`;
|
||||
|
||||
const todosMarcados = filtrados.length > 0 && filtrados.every((u) => lideradosSelecionados.has(u.id));
|
||||
const algumMarcado = filtrados.some((u) => lideradosSelecionados.has(u.id));
|
||||
manageAccountLideradosSelectAll.disabled = filtrados.length === 0;
|
||||
manageAccountLideradosSelectAll.checked = todosMarcados;
|
||||
manageAccountLideradosSelectAll.indeterminate = !todosMarcados && algumMarcado;
|
||||
}
|
||||
|
||||
manageAccountLideradosChecklist.addEventListener("change", (event) => {
|
||||
const checkbox = event.target.closest('input[type="checkbox"]');
|
||||
if (!checkbox) return;
|
||||
const id = Number(checkbox.value);
|
||||
if (checkbox.checked) lideradosSelecionados.add(id);
|
||||
else lideradosSelecionados.delete(id);
|
||||
renderManageAccountLiderados();
|
||||
});
|
||||
|
||||
manageAccountLideradosSearch.addEventListener("input", renderManageAccountLiderados);
|
||||
|
||||
manageAccountLideradosSelectAll.addEventListener("change", () => {
|
||||
const filtrados = lideradosFiltrados();
|
||||
if (manageAccountLideradosSelectAll.checked) {
|
||||
filtrados.forEach((u) => lideradosSelecionados.add(u.id));
|
||||
} else {
|
||||
filtrados.forEach((u) => lideradosSelecionados.delete(u.id));
|
||||
}
|
||||
renderManageAccountLiderados();
|
||||
const seletorLiderados = pidCriarSeletorDuplo({
|
||||
available: {
|
||||
selectAll: document.getElementById("manage-account-liderados-available-select-all"),
|
||||
sortHeader: document.getElementById("manage-account-liderados-available-sort-nome"),
|
||||
searchPrincipal: document.getElementById("manage-account-liderados-available-search-nome"),
|
||||
searchSecundaria: document.getElementById("manage-account-liderados-available-search-departamento"),
|
||||
body: document.getElementById("manage-account-liderados-available-body"),
|
||||
footer: document.getElementById("manage-account-liderados-available-footer"),
|
||||
acaoBtn: document.getElementById("manage-account-liderados-available-link-btn"),
|
||||
},
|
||||
linked: {
|
||||
selectAll: document.getElementById("manage-account-liderados-linked-select-all"),
|
||||
sortHeader: document.getElementById("manage-account-liderados-linked-sort-nome"),
|
||||
searchPrincipal: document.getElementById("manage-account-liderados-linked-search-nome"),
|
||||
searchSecundaria: document.getElementById("manage-account-liderados-linked-search-departamento"),
|
||||
body: document.getElementById("manage-account-liderados-linked-body"),
|
||||
footer: document.getElementById("manage-account-liderados-linked-footer"),
|
||||
acaoBtn: document.getElementById("manage-account-liderados-linked-unlink-btn"),
|
||||
},
|
||||
secundariaValor: departamentosTexto,
|
||||
});
|
||||
|
||||
manageAccountBtn.addEventListener("click", async () => {
|
||||
@ -134,16 +99,16 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
if (me.lideranca) {
|
||||
manageAccountLideradosField.hidden = false;
|
||||
manageAccountSaveBtn.hidden = false;
|
||||
manageAccountLideradosSearch.value = "";
|
||||
lideradosCandidatos = (await pidApiRequest("/usuarios-resumo/"))
|
||||
manageAccountModalCard.classList.add("modal-card--wide");
|
||||
const candidatos = (await pidApiRequest("/usuarios-resumo/"))
|
||||
.filter((u) => u.id !== me.id)
|
||||
.slice()
|
||||
.sort((a, b) => a.nome.localeCompare(b.nome));
|
||||
lideradosSelecionados = new Set((me.liderados || []).map((u) => u.id));
|
||||
renderManageAccountLiderados();
|
||||
seletorLiderados.setDados(candidatos, (me.liderados || []).map((u) => u.id));
|
||||
} else {
|
||||
manageAccountLideradosField.hidden = true;
|
||||
manageAccountSaveBtn.hidden = true;
|
||||
manageAccountModalCard.classList.remove("modal-card--wide");
|
||||
}
|
||||
manageAccountModal.hidden = false;
|
||||
});
|
||||
@ -153,7 +118,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
});
|
||||
|
||||
manageAccountSaveBtn.addEventListener("click", async () => {
|
||||
const liderados = Array.from(lideradosSelecionados);
|
||||
const liderados = seletorLiderados.getVinculadosIds();
|
||||
try {
|
||||
const atualizados = await pidApiRequest("/me/liderados/", { method: "PATCH", body: { liderados } });
|
||||
me.liderados = atualizados;
|
||||
|
||||
152
static/js/dual-select.js
Normal file
152
static/js/dual-select.js
Normal file
@ -0,0 +1,152 @@
|
||||
// Widget genérico de vinculação em duas tabelas — não vinculados (esquerda)
|
||||
// e vinculados (direita), com checkbox de seleção múltipla, busca por coluna,
|
||||
// ordenação por nome e um botão de ação em lote por painel ("Vincular"/
|
||||
// "Desvincular"). Mesmo espírito visual/de interação do painel "Usuários do
|
||||
// Escritório" de perfis-acesso.html (profiles.js), mas genérico o bastante
|
||||
// pra rodar em qualquer shell — hoje reaproveitado por "Usuários sob
|
||||
// liderança" em usuarios.html (users-admin.js) e no modal "Gerenciar
|
||||
// Usuário" (account.js, presente em todo shell).
|
||||
//
|
||||
// A vinculação aqui é só em memória — quem chama decide quando persistir
|
||||
// (chamando getVinculadosIds() no momento de salvar), não dispara nenhuma
|
||||
// requisição por conta própria.
|
||||
//
|
||||
// config = {
|
||||
// available: { selectAll, sortHeader, searchPrincipal, searchSecundaria, body, footer, acaoBtn },
|
||||
// linked: { ...mesmo formato... },
|
||||
// secundariaValor: (candidato) => string,
|
||||
// }
|
||||
// candidato = { id, nome, ...outros campos usados por secundariaValor }
|
||||
function pidCriarSeletorDuplo(config) {
|
||||
const paineis = { available: config.available, linked: config.linked };
|
||||
const secundariaValor = config.secundariaValor || (() => "");
|
||||
const estado = {
|
||||
available: { sortDir: 1, selected: new Set() },
|
||||
linked: { sortDir: 1, selected: new Set() },
|
||||
};
|
||||
|
||||
let candidatos = [];
|
||||
let vinculadosIds = new Set();
|
||||
|
||||
function secundariaTexto(candidato) {
|
||||
return secundariaValor(candidato) || "—";
|
||||
}
|
||||
|
||||
function listaPara(kind) {
|
||||
return kind === "linked"
|
||||
? candidatos.filter((c) => vinculadosIds.has(c.id))
|
||||
: candidatos.filter((c) => !vinculadosIds.has(c.id));
|
||||
}
|
||||
|
||||
function filtradosOrdenados(kind) {
|
||||
const el = paineis[kind];
|
||||
const state = estado[kind];
|
||||
const termoNome = el.searchPrincipal.value.trim().toLowerCase();
|
||||
const termoSecundaria = el.searchSecundaria.value.trim().toLowerCase();
|
||||
return listaPara(kind)
|
||||
.filter((c) => !termoNome || c.nome.toLowerCase().includes(termoNome))
|
||||
.filter((c) => !termoSecundaria || secundariaTexto(c).toLowerCase().includes(termoSecundaria))
|
||||
.slice()
|
||||
.sort((a, b) => a.nome.localeCompare(b.nome, "pt-BR", { sensitivity: "base" }) * state.sortDir);
|
||||
}
|
||||
|
||||
function renderPainel(kind) {
|
||||
const el = paineis[kind];
|
||||
const state = estado[kind];
|
||||
|
||||
// Poda a seleção de ids que saíram da lista (ex.: acabou de ser
|
||||
// vinculado/desvinculado e migrou pro outro painel).
|
||||
const idsAtuais = new Set(listaPara(kind).map((c) => c.id));
|
||||
Array.from(state.selected).forEach((id) => {
|
||||
if (!idsAtuais.has(id)) state.selected.delete(id);
|
||||
});
|
||||
|
||||
const filtrados = filtradosOrdenados(kind);
|
||||
|
||||
el.body.innerHTML = filtrados.length
|
||||
? filtrados
|
||||
.map(
|
||||
(c) => `
|
||||
<tr>
|
||||
<td><input type="checkbox" data-dual-select="${c.id}" ${state.selected.has(c.id) ? "checked" : ""} /></td>
|
||||
<td>${c.nome}</td>
|
||||
<td>${secundariaTexto(c)}</td>
|
||||
</tr>`
|
||||
)
|
||||
.join("")
|
||||
: `<tr class="dual-select__empty-row"><td colspan="3">Nenhum usuário encontrado.</td></tr>`;
|
||||
|
||||
const selecionadosVisiveis = filtrados.filter((c) => state.selected.has(c.id)).length;
|
||||
el.selectAll.disabled = filtrados.length === 0;
|
||||
el.selectAll.checked = filtrados.length > 0 && selecionadosVisiveis === filtrados.length;
|
||||
el.selectAll.indeterminate = selecionadosVisiveis > 0 && selecionadosVisiveis < filtrados.length;
|
||||
|
||||
el.sortHeader.classList.toggle("is-sort-asc", state.sortDir === 1);
|
||||
el.sortHeader.classList.toggle("is-sort-desc", state.sortDir === -1);
|
||||
|
||||
el.footer.textContent = `${filtrados.length} ${filtrados.length === 1 ? "usuário" : "usuários"}`;
|
||||
el.acaoBtn.disabled = state.selected.size === 0;
|
||||
}
|
||||
|
||||
function renderAmbos() {
|
||||
renderPainel("available");
|
||||
renderPainel("linked");
|
||||
}
|
||||
|
||||
function mover(kind) {
|
||||
const state = estado[kind];
|
||||
const ids = Array.from(state.selected);
|
||||
if (!ids.length) return;
|
||||
ids.forEach((id) => {
|
||||
if (kind === "available") vinculadosIds.add(id);
|
||||
else vinculadosIds.delete(id);
|
||||
});
|
||||
state.selected.clear();
|
||||
renderAmbos();
|
||||
}
|
||||
|
||||
["available", "linked"].forEach((kind) => {
|
||||
const el = paineis[kind];
|
||||
el.searchPrincipal.addEventListener("input", () => renderPainel(kind));
|
||||
el.searchSecundaria.addEventListener("input", () => renderPainel(kind));
|
||||
el.sortHeader.addEventListener("click", () => {
|
||||
estado[kind].sortDir *= -1;
|
||||
renderPainel(kind);
|
||||
});
|
||||
el.selectAll.addEventListener("change", () => {
|
||||
const filtrados = filtradosOrdenados(kind);
|
||||
filtrados.forEach((c) => {
|
||||
if (el.selectAll.checked) estado[kind].selected.add(c.id);
|
||||
else estado[kind].selected.delete(c.id);
|
||||
});
|
||||
renderPainel(kind);
|
||||
});
|
||||
el.body.addEventListener("change", (event) => {
|
||||
const checkbox = event.target.closest("[data-dual-select]");
|
||||
if (!checkbox) return;
|
||||
const id = Number(checkbox.getAttribute("data-dual-select"));
|
||||
if (checkbox.checked) estado[kind].selected.add(id);
|
||||
else estado[kind].selected.delete(id);
|
||||
renderPainel(kind);
|
||||
});
|
||||
el.acaoBtn.addEventListener("click", () => mover(kind));
|
||||
});
|
||||
|
||||
function setDados(novosCandidatos, vinculadosIniciais) {
|
||||
candidatos = novosCandidatos;
|
||||
vinculadosIds = new Set(vinculadosIniciais);
|
||||
["available", "linked"].forEach((kind) => {
|
||||
estado[kind].selected.clear();
|
||||
estado[kind].sortDir = 1;
|
||||
paineis[kind].searchPrincipal.value = "";
|
||||
paineis[kind].searchSecundaria.value = "";
|
||||
});
|
||||
renderAmbos();
|
||||
}
|
||||
|
||||
function getVinculadosIds() {
|
||||
return Array.from(vinculadosIds);
|
||||
}
|
||||
|
||||
return { setDados, getVinculadosIds };
|
||||
}
|
||||
@ -150,9 +150,33 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
const moduleListEl = document.getElementById("pa-module-list");
|
||||
const treePanel = document.getElementById("pa-tree-panel");
|
||||
const treeSearchInput = document.getElementById("pa-tree-search-input");
|
||||
const usersAddSelect = document.getElementById("pa-user-add-select");
|
||||
const usersAddBtn = document.getElementById("pa-user-add-btn");
|
||||
const usersListEl = document.getElementById("pa-user-list");
|
||||
const usersPanelsEl = {
|
||||
available: {
|
||||
refreshBtn: document.getElementById("pa-users-available-refresh-btn"),
|
||||
linkBtn: document.getElementById("pa-users-link-btn"),
|
||||
selectAll: document.getElementById("pa-users-available-select-all"),
|
||||
sortHeader: document.getElementById("pa-users-available-sort-nome"),
|
||||
searchNome: document.getElementById("pa-users-available-search-nome"),
|
||||
searchEmail: document.getElementById("pa-users-available-search-email"),
|
||||
body: document.getElementById("pa-users-available-body"),
|
||||
footer: document.getElementById("pa-users-available-footer"),
|
||||
},
|
||||
linked: {
|
||||
refreshBtn: document.getElementById("pa-users-linked-refresh-btn"),
|
||||
linkBtn: document.getElementById("pa-users-unlink-btn"),
|
||||
selectAll: document.getElementById("pa-users-linked-select-all"),
|
||||
sortHeader: document.getElementById("pa-users-linked-sort-nome"),
|
||||
searchNome: document.getElementById("pa-users-linked-search-nome"),
|
||||
searchEmail: document.getElementById("pa-users-linked-search-email"),
|
||||
body: document.getElementById("pa-users-linked-body"),
|
||||
footer: document.getElementById("pa-users-linked-footer"),
|
||||
},
|
||||
};
|
||||
const usersDualState = {
|
||||
available: { sortDir: 1, selected: new Set() },
|
||||
linked: { sortDir: 1, selected: new Set() },
|
||||
};
|
||||
let usuariosCacheAtual = [];
|
||||
const appSearchBtn = document.getElementById("pa-app-search-btn");
|
||||
const appSearchModal = document.getElementById("pa-app-search-modal");
|
||||
const appSearchCloseBtn = document.getElementById("pa-app-search-close-btn");
|
||||
@ -284,36 +308,84 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
`;
|
||||
}
|
||||
|
||||
async function renderUsers() {
|
||||
const usuarios = await pidFetchUsuarios();
|
||||
const isLinked = (u) => u.perfis.some((p) => p.codigo === draftProfile.codigo);
|
||||
const linked = usuarios.filter(isLinked);
|
||||
const unlinked = usuarios.filter((u) => !isLinked(u));
|
||||
function usersDualFiltrados(kind, lista) {
|
||||
const el = usersPanelsEl[kind];
|
||||
const state = usersDualState[kind];
|
||||
const termNome = el.searchNome.value.trim().toLowerCase();
|
||||
const termEmail = el.searchEmail.value.trim().toLowerCase();
|
||||
return lista
|
||||
.filter((u) => !termNome || u.nome.toLowerCase().includes(termNome))
|
||||
.filter((u) => !termEmail || (u.email || "").toLowerCase().includes(termEmail))
|
||||
.slice()
|
||||
.sort((a, b) => a.nome.localeCompare(b.nome, "pt-BR", { sensitivity: "base" }) * state.sortDir);
|
||||
}
|
||||
|
||||
usersListEl.innerHTML = linked.length
|
||||
? linked
|
||||
function renderUsersPanel(kind, listaCompleta) {
|
||||
const el = usersPanelsEl[kind];
|
||||
const state = usersDualState[kind];
|
||||
|
||||
// Poda a seleção de ids que saíram da lista (ex.: acabou de ser
|
||||
// vinculado/desvinculado e migrou pro outro painel).
|
||||
const idsAtuais = new Set(listaCompleta.map((u) => u.id));
|
||||
Array.from(state.selected).forEach((id) => {
|
||||
if (!idsAtuais.has(id)) state.selected.delete(id);
|
||||
});
|
||||
|
||||
const filtrados = usersDualFiltrados(kind, listaCompleta);
|
||||
|
||||
el.body.innerHTML = filtrados.length
|
||||
? filtrados
|
||||
.map(
|
||||
(u) => `
|
||||
<div class="pa-user-row">
|
||||
<span>${u.nome} <span class="pa-user-login">(${u.username})</span></span>
|
||||
<button type="button" class="pa-user-remove" data-pa-user-unlink="${u.id}" aria-label="Remover vínculo">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12" stroke-linecap="round"/></svg>
|
||||
</button>
|
||||
</div>`
|
||||
<tr>
|
||||
<td><input type="checkbox" data-pa-users-select="${u.id}" ${state.selected.has(u.id) ? "checked" : ""} /></td>
|
||||
<td>${u.nome}</td>
|
||||
<td>${u.email || "—"}</td>
|
||||
</tr>`
|
||||
)
|
||||
.join("")
|
||||
: `<p class="pa-users-empty">Nenhum usuário vinculado a este perfil.</p>`;
|
||||
: `<tr class="pa-empty-row"><td colspan="3">Nenhum usuário encontrado.</td></tr>`;
|
||||
|
||||
usersAddSelect.innerHTML = unlinked.length
|
||||
? `<option value="">Selecione um usuário...</option>` +
|
||||
unlinked
|
||||
.map((u) => {
|
||||
const otherNames = u.perfis.map((p) => p.nome);
|
||||
const suffix = otherNames.length ? ` — também em: ${otherNames.join(", ")}` : "";
|
||||
return `<option value="${u.id}">${u.nome} (${u.username})${suffix}</option>`;
|
||||
})
|
||||
.join("")
|
||||
: `<option value="">Nenhum usuário disponível</option>`;
|
||||
const selecionadosVisiveis = filtrados.filter((u) => state.selected.has(u.id)).length;
|
||||
el.selectAll.disabled = filtrados.length === 0;
|
||||
el.selectAll.checked = filtrados.length > 0 && selecionadosVisiveis === filtrados.length;
|
||||
el.selectAll.indeterminate = selecionadosVisiveis > 0 && selecionadosVisiveis < filtrados.length;
|
||||
|
||||
el.sortHeader.classList.toggle("is-sort-asc", state.sortDir === 1);
|
||||
el.sortHeader.classList.toggle("is-sort-desc", state.sortDir === -1);
|
||||
|
||||
el.footer.textContent = `${filtrados.length} ${filtrados.length === 1 ? "usuário" : "usuários"}`;
|
||||
el.linkBtn.disabled = state.selected.size === 0;
|
||||
}
|
||||
|
||||
function listaParaPainelUsuarios(kind) {
|
||||
// Usuário inativo não aparece em nenhum dos dois painéis — decisão
|
||||
// explícita do usuário; inativar já limpa os perfis dele no backend
|
||||
// (ver _revogar_acesso_se_inativo em serializers.py), então isso é
|
||||
// sobretudo defensivo pra dados legados/transição.
|
||||
const ativos = usuariosCacheAtual.filter((u) => u.is_active);
|
||||
const isLinked = (u) => u.perfis.some((p) => p.codigo === draftProfile.codigo);
|
||||
return kind === "linked" ? ativos.filter(isLinked) : ativos.filter((u) => !isLinked(u));
|
||||
}
|
||||
|
||||
function renderUsersFromCache() {
|
||||
renderUsersPanel("available", listaParaPainelUsuarios("available"));
|
||||
renderUsersPanel("linked", listaParaPainelUsuarios("linked"));
|
||||
}
|
||||
|
||||
async function refreshUsuarios() {
|
||||
usuariosCacheAtual = await pidFetchUsuarios();
|
||||
renderUsersFromCache();
|
||||
}
|
||||
|
||||
async function renderUsers() {
|
||||
["available", "linked"].forEach((kind) => {
|
||||
usersDualState[kind].selected.clear();
|
||||
usersDualState[kind].sortDir = 1;
|
||||
usersPanelsEl[kind].searchNome.value = "";
|
||||
usersPanelsEl[kind].searchEmail.value = "";
|
||||
});
|
||||
await refreshUsuarios();
|
||||
}
|
||||
|
||||
function renderTabs() {
|
||||
@ -592,30 +664,56 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
}
|
||||
});
|
||||
|
||||
async function linkUser() {
|
||||
const id = Number(usersAddSelect.value);
|
||||
if (!id) return;
|
||||
const usuarios = await pidFetchUsuarios();
|
||||
const account = usuarios.find((u) => u.id === id);
|
||||
if (!account) return;
|
||||
const codes = account.perfis.map((p) => p.codigo);
|
||||
if (!codes.includes(draftProfile.codigo)) codes.push(draftProfile.codigo);
|
||||
await pidSetUsuarioPerfis(id, codes);
|
||||
await renderUsers();
|
||||
// Vincula (available → linked) ou desvincula (linked → available) todos
|
||||
// os usuários selecionados no painel de uma vez, em paralelo.
|
||||
async function bulkAlterarVinculo(kind, vincular) {
|
||||
const state = usersDualState[kind];
|
||||
const ids = Array.from(state.selected);
|
||||
if (!ids.length) return;
|
||||
const el = usersPanelsEl[kind];
|
||||
el.linkBtn.disabled = true;
|
||||
try {
|
||||
await Promise.all(
|
||||
ids.map((id) => {
|
||||
const account = usuariosCacheAtual.find((u) => u.id === id);
|
||||
if (!account) return Promise.resolve();
|
||||
const codes = account.perfis.map((p) => p.codigo).filter((c) => c !== draftProfile.codigo);
|
||||
if (vincular) codes.push(draftProfile.codigo);
|
||||
return pidSetUsuarioPerfis(id, codes);
|
||||
})
|
||||
);
|
||||
} finally {
|
||||
state.selected.clear();
|
||||
await refreshUsuarios();
|
||||
}
|
||||
}
|
||||
|
||||
usersAddBtn.addEventListener("click", linkUser);
|
||||
|
||||
usersListEl.addEventListener("click", async (event) => {
|
||||
const removeBtn = event.target.closest("[data-pa-user-unlink]");
|
||||
if (!removeBtn) return;
|
||||
const id = Number(removeBtn.getAttribute("data-pa-user-unlink"));
|
||||
const usuarios = await pidFetchUsuarios();
|
||||
const account = usuarios.find((u) => u.id === id);
|
||||
if (!account) return;
|
||||
const codes = account.perfis.map((p) => p.codigo).filter((c) => c !== draftProfile.codigo);
|
||||
await pidSetUsuarioPerfis(id, codes);
|
||||
await renderUsers();
|
||||
["available", "linked"].forEach((kind) => {
|
||||
const el = usersPanelsEl[kind];
|
||||
el.refreshBtn.addEventListener("click", refreshUsuarios);
|
||||
el.searchNome.addEventListener("input", renderUsersFromCache);
|
||||
el.searchEmail.addEventListener("input", renderUsersFromCache);
|
||||
el.sortHeader.addEventListener("click", () => {
|
||||
usersDualState[kind].sortDir *= -1;
|
||||
renderUsersFromCache();
|
||||
});
|
||||
el.selectAll.addEventListener("change", () => {
|
||||
const filtrados = usersDualFiltrados(kind, listaParaPainelUsuarios(kind));
|
||||
filtrados.forEach((u) => {
|
||||
if (el.selectAll.checked) usersDualState[kind].selected.add(u.id);
|
||||
else usersDualState[kind].selected.delete(u.id);
|
||||
});
|
||||
renderUsersFromCache();
|
||||
});
|
||||
el.body.addEventListener("change", (event) => {
|
||||
const checkbox = event.target.closest("[data-pa-users-select]");
|
||||
if (!checkbox) return;
|
||||
const id = Number(checkbox.getAttribute("data-pa-users-select"));
|
||||
if (checkbox.checked) usersDualState[kind].selected.add(id);
|
||||
else usersDualState[kind].selected.delete(id);
|
||||
renderUsersFromCache();
|
||||
});
|
||||
el.linkBtn.addEventListener("click", () => bulkAlterarVinculo(kind, kind === "available"));
|
||||
});
|
||||
|
||||
saveBtn.addEventListener("click", async () => {
|
||||
|
||||
@ -30,6 +30,9 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
const searchInput = document.getElementById("ua-search-input");
|
||||
const statusFiltros = document.getElementById("ua-status-filtros");
|
||||
let statusFiltro = "ativos";
|
||||
const sortHeaders = Array.from(document.querySelectorAll("#ua-table th[data-sort]"));
|
||||
let sortKey = "nome";
|
||||
let sortDir = 1;
|
||||
const newBtn = document.getElementById("ua-new-btn");
|
||||
const backBtn = document.getElementById("ua-back-btn");
|
||||
const cancelBtn = document.getElementById("ua-cancel-btn");
|
||||
@ -54,9 +57,32 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
const ativoInput = document.getElementById("ua-ativo");
|
||||
const liderancaInput = document.getElementById("ua-lideranca");
|
||||
const lideradosField = document.getElementById("ua-liderados-field");
|
||||
const lideradosSearchInput = document.getElementById("ua-liderados-search");
|
||||
const lideradosSelectAllInput = document.getElementById("ua-liderados-select-all");
|
||||
const lideradosChecklist = document.getElementById("ua-liderados-checklist");
|
||||
|
||||
function departamentosTexto(usuario) {
|
||||
return (usuario.departamentos || []).map((d) => d.nome).join(", ");
|
||||
}
|
||||
|
||||
const seletorLiderados = pidCriarSeletorDuplo({
|
||||
available: {
|
||||
selectAll: document.getElementById("ua-liderados-available-select-all"),
|
||||
sortHeader: document.getElementById("ua-liderados-available-sort-nome"),
|
||||
searchPrincipal: document.getElementById("ua-liderados-available-search-nome"),
|
||||
searchSecundaria: document.getElementById("ua-liderados-available-search-departamento"),
|
||||
body: document.getElementById("ua-liderados-available-body"),
|
||||
footer: document.getElementById("ua-liderados-available-footer"),
|
||||
acaoBtn: document.getElementById("ua-liderados-available-link-btn"),
|
||||
},
|
||||
linked: {
|
||||
selectAll: document.getElementById("ua-liderados-linked-select-all"),
|
||||
sortHeader: document.getElementById("ua-liderados-linked-sort-nome"),
|
||||
searchPrincipal: document.getElementById("ua-liderados-linked-search-nome"),
|
||||
searchSecundaria: document.getElementById("ua-liderados-linked-search-departamento"),
|
||||
body: document.getElementById("ua-liderados-linked-body"),
|
||||
footer: document.getElementById("ua-liderados-linked-footer"),
|
||||
acaoBtn: document.getElementById("ua-liderados-linked-unlink-btn"),
|
||||
},
|
||||
secundariaValor: departamentosTexto,
|
||||
});
|
||||
|
||||
liderancaInput.addEventListener("change", () => {
|
||||
lideradosField.hidden = !liderancaInput.checked;
|
||||
@ -68,13 +94,49 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
return perfis.length ? perfis.map((p) => p.nome).join(", ") : "Sem perfil vinculado";
|
||||
}
|
||||
|
||||
// Compara duas strings tratando conteúdo numérico como número (ex.:
|
||||
// "92" < "150") e caindo pra comparação alfabética quando não são os dois
|
||||
// numéricos — mesmo critério de comparaValoresLista() em
|
||||
// importacao-plano-saude.js.
|
||||
function comparaValoresUsuario(a, b) {
|
||||
const an = Number(a);
|
||||
const bn = Number(b);
|
||||
const ambosNumeros = a !== "" && b !== "" && !Number.isNaN(an) && !Number.isNaN(bn);
|
||||
if (ambosNumeros) return an - bn;
|
||||
return String(a).localeCompare(String(b), "pt-BR", { sensitivity: "base" });
|
||||
}
|
||||
|
||||
const PID_UA_SORT_VALOR = {
|
||||
username: (u) => u.username || "",
|
||||
nome: (u) => u.nome || "",
|
||||
codigo_folha: (u) => u.codigo_folha || "",
|
||||
codigo_questor: (u) => u.codigo_questor || "",
|
||||
codigo_tareffa: (u) => u.codigo_tareffa || "",
|
||||
codigo_contabit: (u) => u.codigo_contabit || "",
|
||||
ramal: (u) => u.ramal || "",
|
||||
status: (u) => (u.is_active ? 1 : 0),
|
||||
};
|
||||
|
||||
// Célula vazia fica destacada (itálico, cor apagada) — ordenar a coluna
|
||||
// agrupa quem ainda não tem aquele código/ramal cadastrado.
|
||||
function celulaCodigo(valor) {
|
||||
return valor ? valor : '<span class="ua-campo-vazio">—</span>';
|
||||
}
|
||||
|
||||
function renderList() {
|
||||
const term = searchInput.value.trim().toLowerCase();
|
||||
const extrator = PID_UA_SORT_VALOR[sortKey];
|
||||
const rows = users
|
||||
.filter((u) => statusFiltro === "todos" || (statusFiltro === "ativos") === u.is_active)
|
||||
.filter((u) => !term || u.nome.toLowerCase().includes(term) || u.username.toLowerCase().includes(term))
|
||||
.slice()
|
||||
.sort((a, b) => a.nome.localeCompare(b.nome));
|
||||
.sort((a, b) => comparaValoresUsuario(extrator(a), extrator(b)) * sortDir);
|
||||
|
||||
sortHeaders.forEach((th) => {
|
||||
const key = th.getAttribute("data-sort");
|
||||
th.classList.toggle("is-sort-asc", key === sortKey && sortDir === 1);
|
||||
th.classList.toggle("is-sort-desc", key === sortKey && sortDir === -1);
|
||||
});
|
||||
|
||||
tableBody.innerHTML = rows.length
|
||||
? rows
|
||||
@ -83,6 +145,11 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
<tr>
|
||||
<td>${u.username}</td>
|
||||
<td><a href="#" class="pa-name-link" data-ua-edit="${u.id}">${u.nome}</a></td>
|
||||
<td>${celulaCodigo(u.codigo_folha)}</td>
|
||||
<td>${celulaCodigo(u.codigo_questor)}</td>
|
||||
<td>${celulaCodigo(u.codigo_tareffa)}</td>
|
||||
<td>${celulaCodigo(u.codigo_contabit)}</td>
|
||||
<td>${celulaCodigo(u.ramal)}</td>
|
||||
<td>${profileNomes(u.perfis)}</td>
|
||||
<td><span class="status-pill ${u.is_active ? "status-pill--ativo" : "status-pill--inativo"}">${u.is_active ? "Ativo" : "Inativo"}</span></td>
|
||||
<td>
|
||||
@ -101,7 +168,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
</tr>`
|
||||
)
|
||||
.join("")
|
||||
: `<tr class="pa-empty-row"><td colspan="5">Nenhum usuário encontrado.</td></tr>`;
|
||||
: `<tr class="pa-empty-row"><td colspan="10">Nenhum usuário encontrado.</td></tr>`;
|
||||
|
||||
footerCount.textContent = `${rows.length} ${rows.length === 1 ? "item" : "itens"}`;
|
||||
}
|
||||
@ -119,79 +186,21 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
.join("");
|
||||
}
|
||||
|
||||
let lideradosCandidatos = [];
|
||||
let lideradosSelecionados = new Set();
|
||||
|
||||
function departamentosTexto(usuario) {
|
||||
return (usuario.departamentos || []).map((d) => d.nome).join(", ");
|
||||
}
|
||||
|
||||
function lideradosFiltrados() {
|
||||
const termo = lideradosSearchInput.value.trim().toLowerCase();
|
||||
return lideradosCandidatos.filter(
|
||||
(u) => !termo || u.nome.toLowerCase().includes(termo) || departamentosTexto(u).toLowerCase().includes(termo)
|
||||
);
|
||||
}
|
||||
|
||||
function renderLideradosChecklist() {
|
||||
const filtrados = lideradosFiltrados();
|
||||
lideradosChecklist.innerHTML = filtrados.length
|
||||
? filtrados
|
||||
.map(
|
||||
(u) => `
|
||||
<label class="checklist-item">
|
||||
<input type="checkbox" value="${u.id}" ${lideradosSelecionados.has(u.id) ? "checked" : ""} />
|
||||
<span class="checklist-item__info">
|
||||
<span class="checklist-item__nome">${u.nome}</span>
|
||||
<span class="checklist-item__departamento">${departamentosTexto(u) || "—"}</span>
|
||||
</span>
|
||||
</label>`
|
||||
)
|
||||
.join("")
|
||||
: `<p class="checklist-empty">Nenhum usuário encontrado.</p>`;
|
||||
|
||||
const todosMarcados = filtrados.length > 0 && filtrados.every((u) => lideradosSelecionados.has(u.id));
|
||||
const algumMarcado = filtrados.some((u) => lideradosSelecionados.has(u.id));
|
||||
lideradosSelectAllInput.disabled = filtrados.length === 0;
|
||||
lideradosSelectAllInput.checked = todosMarcados;
|
||||
lideradosSelectAllInput.indeterminate = !todosMarcados && algumMarcado;
|
||||
}
|
||||
|
||||
function fillLideradosChecklist(selectedIds, excludeId) {
|
||||
lideradosCandidatos = users
|
||||
.filter((u) => u.id !== excludeId)
|
||||
// Usuário inativo não entra como candidato (nem aparece do lado
|
||||
// vinculado) — mesma decisão do widget "Usuários do Escritório" de
|
||||
// Perfis de Acesso (ver profiles.js).
|
||||
const candidatos = users
|
||||
.filter((u) => u.id !== excludeId && u.is_active)
|
||||
.slice()
|
||||
.sort((a, b) => a.nome.localeCompare(b.nome));
|
||||
lideradosSelecionados = new Set(selectedIds);
|
||||
lideradosSearchInput.value = "";
|
||||
renderLideradosChecklist();
|
||||
seletorLiderados.setDados(candidatos, selectedIds);
|
||||
}
|
||||
|
||||
function selectedLideradosIds() {
|
||||
return Array.from(lideradosSelecionados);
|
||||
return seletorLiderados.getVinculadosIds();
|
||||
}
|
||||
|
||||
lideradosChecklist.addEventListener("change", (event) => {
|
||||
const checkbox = event.target.closest('input[type="checkbox"]');
|
||||
if (!checkbox) return;
|
||||
const id = Number(checkbox.value);
|
||||
if (checkbox.checked) lideradosSelecionados.add(id);
|
||||
else lideradosSelecionados.delete(id);
|
||||
renderLideradosChecklist();
|
||||
});
|
||||
|
||||
lideradosSearchInput.addEventListener("input", renderLideradosChecklist);
|
||||
|
||||
lideradosSelectAllInput.addEventListener("change", () => {
|
||||
const filtrados = lideradosFiltrados();
|
||||
if (lideradosSelectAllInput.checked) {
|
||||
filtrados.forEach((u) => lideradosSelecionados.add(u.id));
|
||||
} else {
|
||||
filtrados.forEach((u) => lideradosSelecionados.delete(u.id));
|
||||
}
|
||||
renderLideradosChecklist();
|
||||
});
|
||||
|
||||
let departamentosCache = [];
|
||||
|
||||
async function fillDepartamentoChecklist(selectedIds) {
|
||||
@ -284,6 +293,18 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
}
|
||||
|
||||
searchInput.addEventListener("input", renderList);
|
||||
sortHeaders.forEach((th) => {
|
||||
th.addEventListener("click", () => {
|
||||
const key = th.getAttribute("data-sort");
|
||||
if (sortKey === key) {
|
||||
sortDir *= -1;
|
||||
} else {
|
||||
sortKey = key;
|
||||
sortDir = 1;
|
||||
}
|
||||
renderList();
|
||||
});
|
||||
});
|
||||
statusFiltros.addEventListener("click", (event) => {
|
||||
const chip = event.target.closest("[data-ua-status]");
|
||||
if (!chip) return;
|
||||
|
||||
@ -377,19 +377,62 @@
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="manage-account-modal" hidden>
|
||||
<div class="modal-card">
|
||||
<div class="modal-card" id="manage-account-modal-card">
|
||||
<h2 class="modal-card__title">Gerenciar Usuário</h2>
|
||||
<div class="modal-field">
|
||||
<button type="button" class="btn-outline" id="manage-account-change-password-btn">Alterar senha</button>
|
||||
</div>
|
||||
<div class="modal-field" id="manage-account-liderados-field" hidden>
|
||||
<label>Usuários sob sua liderança</label>
|
||||
<input type="text" id="manage-account-liderados-search" class="checklist-search" placeholder="Buscar por nome ou departamento..." autocomplete="off" />
|
||||
<label class="modal-checkbox checklist-select-all">
|
||||
<input type="checkbox" id="manage-account-liderados-select-all" />
|
||||
Marcar todos os resultados da busca
|
||||
</label>
|
||||
<div class="checklist-box" id="manage-account-liderados-checklist"></div>
|
||||
<div class="dual-select">
|
||||
<div class="dual-select__panel">
|
||||
<div class="dual-select__panel-header">
|
||||
<button type="button" class="btn-solid" id="manage-account-liderados-available-link-btn" disabled>Vincular</button>
|
||||
</div>
|
||||
<div class="dual-select__table-wrap">
|
||||
<table class="dual-select__table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" id="manage-account-liderados-available-select-all" aria-label="Selecionar todos" /></th>
|
||||
<th class="dual-select__sort" id="manage-account-liderados-available-sort-nome">Nome <span class="dual-select__sort-icon">↕</span></th>
|
||||
<th>Departamento</th>
|
||||
</tr>
|
||||
<tr class="dual-select__filters">
|
||||
<th></th>
|
||||
<th><input type="text" id="manage-account-liderados-available-search-nome" placeholder="Buscar por nome..." autocomplete="off" /></th>
|
||||
<th><input type="text" id="manage-account-liderados-available-search-departamento" placeholder="Buscar por departamento..." autocomplete="off" /></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="manage-account-liderados-available-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="dual-select__footer" id="manage-account-liderados-available-footer"></div>
|
||||
</div>
|
||||
|
||||
<div class="dual-select__panel">
|
||||
<div class="dual-select__panel-header">
|
||||
<button type="button" class="btn-danger-outline" id="manage-account-liderados-linked-unlink-btn" disabled>Desvincular</button>
|
||||
</div>
|
||||
<div class="dual-select__table-wrap">
|
||||
<table class="dual-select__table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" id="manage-account-liderados-linked-select-all" aria-label="Selecionar todos" /></th>
|
||||
<th class="dual-select__sort" id="manage-account-liderados-linked-sort-nome">Nome <span class="dual-select__sort-icon">↕</span></th>
|
||||
<th>Departamento</th>
|
||||
</tr>
|
||||
<tr class="dual-select__filters">
|
||||
<th></th>
|
||||
<th><input type="text" id="manage-account-liderados-linked-search-nome" placeholder="Buscar por nome..." autocomplete="off" /></th>
|
||||
<th><input type="text" id="manage-account-liderados-linked-search-departamento" placeholder="Buscar por departamento..." autocomplete="off" /></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="manage-account-liderados-linked-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="dual-select__footer" id="manage-account-liderados-linked-footer"></div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="modal-error" id="manage-account-liderados-error"></p>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
@ -454,6 +497,7 @@
|
||||
</div>
|
||||
|
||||
<script src="{% static 'js/api.js' %}"></script>
|
||||
<script src="{% static 'js/dual-select.js' %}"></script>
|
||||
<script src="{% static 'js/profiles.js' %}"></script>
|
||||
<script src="{% static 'js/auth.js' %}"></script>
|
||||
<script src="{% static 'js/access.js' %}"></script>
|
||||
|
||||
@ -513,19 +513,62 @@
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="manage-account-modal" hidden>
|
||||
<div class="modal-card">
|
||||
<div class="modal-card" id="manage-account-modal-card">
|
||||
<h2 class="modal-card__title">Gerenciar Usuário</h2>
|
||||
<div class="modal-field">
|
||||
<button type="button" class="btn-outline" id="manage-account-change-password-btn">Alterar senha</button>
|
||||
</div>
|
||||
<div class="modal-field" id="manage-account-liderados-field" hidden>
|
||||
<label>Usuários sob sua liderança</label>
|
||||
<input type="text" id="manage-account-liderados-search" class="checklist-search" placeholder="Buscar por nome ou departamento..." autocomplete="off" />
|
||||
<label class="modal-checkbox checklist-select-all">
|
||||
<input type="checkbox" id="manage-account-liderados-select-all" />
|
||||
Marcar todos os resultados da busca
|
||||
</label>
|
||||
<div class="checklist-box" id="manage-account-liderados-checklist"></div>
|
||||
<div class="dual-select">
|
||||
<div class="dual-select__panel">
|
||||
<div class="dual-select__panel-header">
|
||||
<button type="button" class="btn-solid" id="manage-account-liderados-available-link-btn" disabled>Vincular</button>
|
||||
</div>
|
||||
<div class="dual-select__table-wrap">
|
||||
<table class="dual-select__table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" id="manage-account-liderados-available-select-all" aria-label="Selecionar todos" /></th>
|
||||
<th class="dual-select__sort" id="manage-account-liderados-available-sort-nome">Nome <span class="dual-select__sort-icon">↕</span></th>
|
||||
<th>Departamento</th>
|
||||
</tr>
|
||||
<tr class="dual-select__filters">
|
||||
<th></th>
|
||||
<th><input type="text" id="manage-account-liderados-available-search-nome" placeholder="Buscar por nome..." autocomplete="off" /></th>
|
||||
<th><input type="text" id="manage-account-liderados-available-search-departamento" placeholder="Buscar por departamento..." autocomplete="off" /></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="manage-account-liderados-available-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="dual-select__footer" id="manage-account-liderados-available-footer"></div>
|
||||
</div>
|
||||
|
||||
<div class="dual-select__panel">
|
||||
<div class="dual-select__panel-header">
|
||||
<button type="button" class="btn-danger-outline" id="manage-account-liderados-linked-unlink-btn" disabled>Desvincular</button>
|
||||
</div>
|
||||
<div class="dual-select__table-wrap">
|
||||
<table class="dual-select__table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" id="manage-account-liderados-linked-select-all" aria-label="Selecionar todos" /></th>
|
||||
<th class="dual-select__sort" id="manage-account-liderados-linked-sort-nome">Nome <span class="dual-select__sort-icon">↕</span></th>
|
||||
<th>Departamento</th>
|
||||
</tr>
|
||||
<tr class="dual-select__filters">
|
||||
<th></th>
|
||||
<th><input type="text" id="manage-account-liderados-linked-search-nome" placeholder="Buscar por nome..." autocomplete="off" /></th>
|
||||
<th><input type="text" id="manage-account-liderados-linked-search-departamento" placeholder="Buscar por departamento..." autocomplete="off" /></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="manage-account-liderados-linked-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="dual-select__footer" id="manage-account-liderados-linked-footer"></div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="modal-error" id="manage-account-liderados-error"></p>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
@ -590,6 +633,7 @@
|
||||
</div>
|
||||
|
||||
<script src="{% static 'js/api.js' %}"></script>
|
||||
<script src="{% static 'js/dual-select.js' %}"></script>
|
||||
<script src="{% static 'js/profiles.js' %}"></script>
|
||||
<script src="{% static 'js/auth.js' %}"></script>
|
||||
<script src="{% static 'js/access.js' %}"></script>
|
||||
|
||||
@ -499,19 +499,62 @@
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="manage-account-modal" hidden>
|
||||
<div class="modal-card">
|
||||
<div class="modal-card" id="manage-account-modal-card">
|
||||
<h2 class="modal-card__title">Gerenciar Usuário</h2>
|
||||
<div class="modal-field">
|
||||
<button type="button" class="btn-outline" id="manage-account-change-password-btn">Alterar senha</button>
|
||||
</div>
|
||||
<div class="modal-field" id="manage-account-liderados-field" hidden>
|
||||
<label>Usuários sob sua liderança</label>
|
||||
<input type="text" id="manage-account-liderados-search" class="checklist-search" placeholder="Buscar por nome ou departamento..." autocomplete="off" />
|
||||
<label class="modal-checkbox checklist-select-all">
|
||||
<input type="checkbox" id="manage-account-liderados-select-all" />
|
||||
Marcar todos os resultados da busca
|
||||
</label>
|
||||
<div class="checklist-box" id="manage-account-liderados-checklist"></div>
|
||||
<div class="dual-select">
|
||||
<div class="dual-select__panel">
|
||||
<div class="dual-select__panel-header">
|
||||
<button type="button" class="btn-solid" id="manage-account-liderados-available-link-btn" disabled>Vincular</button>
|
||||
</div>
|
||||
<div class="dual-select__table-wrap">
|
||||
<table class="dual-select__table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" id="manage-account-liderados-available-select-all" aria-label="Selecionar todos" /></th>
|
||||
<th class="dual-select__sort" id="manage-account-liderados-available-sort-nome">Nome <span class="dual-select__sort-icon">↕</span></th>
|
||||
<th>Departamento</th>
|
||||
</tr>
|
||||
<tr class="dual-select__filters">
|
||||
<th></th>
|
||||
<th><input type="text" id="manage-account-liderados-available-search-nome" placeholder="Buscar por nome..." autocomplete="off" /></th>
|
||||
<th><input type="text" id="manage-account-liderados-available-search-departamento" placeholder="Buscar por departamento..." autocomplete="off" /></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="manage-account-liderados-available-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="dual-select__footer" id="manage-account-liderados-available-footer"></div>
|
||||
</div>
|
||||
|
||||
<div class="dual-select__panel">
|
||||
<div class="dual-select__panel-header">
|
||||
<button type="button" class="btn-danger-outline" id="manage-account-liderados-linked-unlink-btn" disabled>Desvincular</button>
|
||||
</div>
|
||||
<div class="dual-select__table-wrap">
|
||||
<table class="dual-select__table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" id="manage-account-liderados-linked-select-all" aria-label="Selecionar todos" /></th>
|
||||
<th class="dual-select__sort" id="manage-account-liderados-linked-sort-nome">Nome <span class="dual-select__sort-icon">↕</span></th>
|
||||
<th>Departamento</th>
|
||||
</tr>
|
||||
<tr class="dual-select__filters">
|
||||
<th></th>
|
||||
<th><input type="text" id="manage-account-liderados-linked-search-nome" placeholder="Buscar por nome..." autocomplete="off" /></th>
|
||||
<th><input type="text" id="manage-account-liderados-linked-search-departamento" placeholder="Buscar por departamento..." autocomplete="off" /></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="manage-account-liderados-linked-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="dual-select__footer" id="manage-account-liderados-linked-footer"></div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="modal-error" id="manage-account-liderados-error"></p>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
@ -576,6 +619,7 @@
|
||||
</div>
|
||||
|
||||
<script src="{% static 'js/api.js' %}"></script>
|
||||
<script src="{% static 'js/dual-select.js' %}"></script>
|
||||
<script src="{% static 'js/profiles.js' %}"></script>
|
||||
<script src="{% static 'js/auth.js' %}"></script>
|
||||
<script src="{% static 'js/access.js' %}"></script>
|
||||
|
||||
@ -577,19 +577,62 @@
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="manage-account-modal" hidden>
|
||||
<div class="modal-card">
|
||||
<div class="modal-card" id="manage-account-modal-card">
|
||||
<h2 class="modal-card__title">Gerenciar Usuário</h2>
|
||||
<div class="modal-field">
|
||||
<button type="button" class="btn-outline" id="manage-account-change-password-btn">Alterar senha</button>
|
||||
</div>
|
||||
<div class="modal-field" id="manage-account-liderados-field" hidden>
|
||||
<label>Usuários sob sua liderança</label>
|
||||
<input type="text" id="manage-account-liderados-search" class="checklist-search" placeholder="Buscar por nome ou departamento..." autocomplete="off" />
|
||||
<label class="modal-checkbox checklist-select-all">
|
||||
<input type="checkbox" id="manage-account-liderados-select-all" />
|
||||
Marcar todos os resultados da busca
|
||||
</label>
|
||||
<div class="checklist-box" id="manage-account-liderados-checklist"></div>
|
||||
<div class="dual-select">
|
||||
<div class="dual-select__panel">
|
||||
<div class="dual-select__panel-header">
|
||||
<button type="button" class="btn-solid" id="manage-account-liderados-available-link-btn" disabled>Vincular</button>
|
||||
</div>
|
||||
<div class="dual-select__table-wrap">
|
||||
<table class="dual-select__table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" id="manage-account-liderados-available-select-all" aria-label="Selecionar todos" /></th>
|
||||
<th class="dual-select__sort" id="manage-account-liderados-available-sort-nome">Nome <span class="dual-select__sort-icon">↕</span></th>
|
||||
<th>Departamento</th>
|
||||
</tr>
|
||||
<tr class="dual-select__filters">
|
||||
<th></th>
|
||||
<th><input type="text" id="manage-account-liderados-available-search-nome" placeholder="Buscar por nome..." autocomplete="off" /></th>
|
||||
<th><input type="text" id="manage-account-liderados-available-search-departamento" placeholder="Buscar por departamento..." autocomplete="off" /></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="manage-account-liderados-available-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="dual-select__footer" id="manage-account-liderados-available-footer"></div>
|
||||
</div>
|
||||
|
||||
<div class="dual-select__panel">
|
||||
<div class="dual-select__panel-header">
|
||||
<button type="button" class="btn-danger-outline" id="manage-account-liderados-linked-unlink-btn" disabled>Desvincular</button>
|
||||
</div>
|
||||
<div class="dual-select__table-wrap">
|
||||
<table class="dual-select__table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" id="manage-account-liderados-linked-select-all" aria-label="Selecionar todos" /></th>
|
||||
<th class="dual-select__sort" id="manage-account-liderados-linked-sort-nome">Nome <span class="dual-select__sort-icon">↕</span></th>
|
||||
<th>Departamento</th>
|
||||
</tr>
|
||||
<tr class="dual-select__filters">
|
||||
<th></th>
|
||||
<th><input type="text" id="manage-account-liderados-linked-search-nome" placeholder="Buscar por nome..." autocomplete="off" /></th>
|
||||
<th><input type="text" id="manage-account-liderados-linked-search-departamento" placeholder="Buscar por departamento..." autocomplete="off" /></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="manage-account-liderados-linked-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="dual-select__footer" id="manage-account-liderados-linked-footer"></div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="modal-error" id="manage-account-liderados-error"></p>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
@ -654,6 +697,7 @@
|
||||
</div>
|
||||
|
||||
<script src="{% static 'js/api.js' %}"></script>
|
||||
<script src="{% static 'js/dual-select.js' %}"></script>
|
||||
<script src="{% static 'js/profiles.js' %}"></script>
|
||||
<script src="{% static 'js/auth.js' %}"></script>
|
||||
<script src="{% static 'js/access.js' %}"></script>
|
||||
|
||||
@ -753,19 +753,62 @@
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="manage-account-modal" hidden>
|
||||
<div class="modal-card">
|
||||
<div class="modal-card" id="manage-account-modal-card">
|
||||
<h2 class="modal-card__title">Gerenciar Usuário</h2>
|
||||
<div class="modal-field">
|
||||
<button type="button" class="btn-outline" id="manage-account-change-password-btn">Alterar senha</button>
|
||||
</div>
|
||||
<div class="modal-field" id="manage-account-liderados-field" hidden>
|
||||
<label>Usuários sob sua liderança</label>
|
||||
<input type="text" id="manage-account-liderados-search" class="checklist-search" placeholder="Buscar por nome ou departamento..." autocomplete="off" />
|
||||
<label class="modal-checkbox checklist-select-all">
|
||||
<input type="checkbox" id="manage-account-liderados-select-all" />
|
||||
Marcar todos os resultados da busca
|
||||
</label>
|
||||
<div class="checklist-box" id="manage-account-liderados-checklist"></div>
|
||||
<div class="dual-select">
|
||||
<div class="dual-select__panel">
|
||||
<div class="dual-select__panel-header">
|
||||
<button type="button" class="btn-solid" id="manage-account-liderados-available-link-btn" disabled>Vincular</button>
|
||||
</div>
|
||||
<div class="dual-select__table-wrap">
|
||||
<table class="dual-select__table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" id="manage-account-liderados-available-select-all" aria-label="Selecionar todos" /></th>
|
||||
<th class="dual-select__sort" id="manage-account-liderados-available-sort-nome">Nome <span class="dual-select__sort-icon">↕</span></th>
|
||||
<th>Departamento</th>
|
||||
</tr>
|
||||
<tr class="dual-select__filters">
|
||||
<th></th>
|
||||
<th><input type="text" id="manage-account-liderados-available-search-nome" placeholder="Buscar por nome..." autocomplete="off" /></th>
|
||||
<th><input type="text" id="manage-account-liderados-available-search-departamento" placeholder="Buscar por departamento..." autocomplete="off" /></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="manage-account-liderados-available-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="dual-select__footer" id="manage-account-liderados-available-footer"></div>
|
||||
</div>
|
||||
|
||||
<div class="dual-select__panel">
|
||||
<div class="dual-select__panel-header">
|
||||
<button type="button" class="btn-danger-outline" id="manage-account-liderados-linked-unlink-btn" disabled>Desvincular</button>
|
||||
</div>
|
||||
<div class="dual-select__table-wrap">
|
||||
<table class="dual-select__table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" id="manage-account-liderados-linked-select-all" aria-label="Selecionar todos" /></th>
|
||||
<th class="dual-select__sort" id="manage-account-liderados-linked-sort-nome">Nome <span class="dual-select__sort-icon">↕</span></th>
|
||||
<th>Departamento</th>
|
||||
</tr>
|
||||
<tr class="dual-select__filters">
|
||||
<th></th>
|
||||
<th><input type="text" id="manage-account-liderados-linked-search-nome" placeholder="Buscar por nome..." autocomplete="off" /></th>
|
||||
<th><input type="text" id="manage-account-liderados-linked-search-departamento" placeholder="Buscar por departamento..." autocomplete="off" /></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="manage-account-liderados-linked-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="dual-select__footer" id="manage-account-liderados-linked-footer"></div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="modal-error" id="manage-account-liderados-error"></p>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
@ -830,6 +873,7 @@
|
||||
</div>
|
||||
|
||||
<script src="{% static 'js/api.js' %}"></script>
|
||||
<script src="{% static 'js/dual-select.js' %}"></script>
|
||||
<script src="{% static 'js/profiles.js' %}"></script>
|
||||
<script src="{% static 'js/auth.js' %}"></script>
|
||||
<script src="{% static 'js/access.js' %}"></script>
|
||||
|
||||
@ -363,19 +363,62 @@
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="manage-account-modal" hidden>
|
||||
<div class="modal-card">
|
||||
<div class="modal-card" id="manage-account-modal-card">
|
||||
<h2 class="modal-card__title">Gerenciar Usuário</h2>
|
||||
<div class="modal-field">
|
||||
<button type="button" class="btn-outline" id="manage-account-change-password-btn">Alterar senha</button>
|
||||
</div>
|
||||
<div class="modal-field" id="manage-account-liderados-field" hidden>
|
||||
<label>Usuários sob sua liderança</label>
|
||||
<input type="text" id="manage-account-liderados-search" class="checklist-search" placeholder="Buscar por nome ou departamento..." autocomplete="off" />
|
||||
<label class="modal-checkbox checklist-select-all">
|
||||
<input type="checkbox" id="manage-account-liderados-select-all" />
|
||||
Marcar todos os resultados da busca
|
||||
</label>
|
||||
<div class="checklist-box" id="manage-account-liderados-checklist"></div>
|
||||
<div class="dual-select">
|
||||
<div class="dual-select__panel">
|
||||
<div class="dual-select__panel-header">
|
||||
<button type="button" class="btn-solid" id="manage-account-liderados-available-link-btn" disabled>Vincular</button>
|
||||
</div>
|
||||
<div class="dual-select__table-wrap">
|
||||
<table class="dual-select__table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" id="manage-account-liderados-available-select-all" aria-label="Selecionar todos" /></th>
|
||||
<th class="dual-select__sort" id="manage-account-liderados-available-sort-nome">Nome <span class="dual-select__sort-icon">↕</span></th>
|
||||
<th>Departamento</th>
|
||||
</tr>
|
||||
<tr class="dual-select__filters">
|
||||
<th></th>
|
||||
<th><input type="text" id="manage-account-liderados-available-search-nome" placeholder="Buscar por nome..." autocomplete="off" /></th>
|
||||
<th><input type="text" id="manage-account-liderados-available-search-departamento" placeholder="Buscar por departamento..." autocomplete="off" /></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="manage-account-liderados-available-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="dual-select__footer" id="manage-account-liderados-available-footer"></div>
|
||||
</div>
|
||||
|
||||
<div class="dual-select__panel">
|
||||
<div class="dual-select__panel-header">
|
||||
<button type="button" class="btn-danger-outline" id="manage-account-liderados-linked-unlink-btn" disabled>Desvincular</button>
|
||||
</div>
|
||||
<div class="dual-select__table-wrap">
|
||||
<table class="dual-select__table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" id="manage-account-liderados-linked-select-all" aria-label="Selecionar todos" /></th>
|
||||
<th class="dual-select__sort" id="manage-account-liderados-linked-sort-nome">Nome <span class="dual-select__sort-icon">↕</span></th>
|
||||
<th>Departamento</th>
|
||||
</tr>
|
||||
<tr class="dual-select__filters">
|
||||
<th></th>
|
||||
<th><input type="text" id="manage-account-liderados-linked-search-nome" placeholder="Buscar por nome..." autocomplete="off" /></th>
|
||||
<th><input type="text" id="manage-account-liderados-linked-search-departamento" placeholder="Buscar por departamento..." autocomplete="off" /></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="manage-account-liderados-linked-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="dual-select__footer" id="manage-account-liderados-linked-footer"></div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="modal-error" id="manage-account-liderados-error"></p>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
@ -440,6 +483,7 @@
|
||||
</div>
|
||||
|
||||
<script src="{% static 'js/api.js' %}"></script>
|
||||
<script src="{% static 'js/dual-select.js' %}"></script>
|
||||
<script src="{% static 'js/profiles.js' %}"></script>
|
||||
<script src="{% static 'js/auth.js' %}"></script>
|
||||
<script src="{% static 'js/access.js' %}"></script>
|
||||
|
||||
@ -328,11 +328,61 @@
|
||||
</div>
|
||||
|
||||
<div class="pa-tab-panel" data-pa-tab-panel="usuarios" hidden>
|
||||
<div class="pa-users-add">
|
||||
<select id="pa-user-add-select"></select>
|
||||
<button type="button" class="btn-solid" id="pa-user-add-btn">Vincular</button>
|
||||
<div class="pa-users-dual">
|
||||
<div class="pa-users-panel">
|
||||
<div class="pa-users-panel__header">
|
||||
<button type="button" class="icon-btn" id="pa-users-available-refresh-btn" aria-label="Atualizar lista">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 4v6h-6M1 20v-6h6" stroke-linecap="round" stroke-linejoin="round"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
</button>
|
||||
<button type="button" class="btn-solid" id="pa-users-link-btn" disabled>Vincular</button>
|
||||
</div>
|
||||
<div class="pa-table-wrap">
|
||||
<table class="pa-table pa-users-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="pa-users-table__checkbox"><input type="checkbox" id="pa-users-available-select-all" aria-label="Selecionar todos" /></th>
|
||||
<th class="pa-users-table__sort" id="pa-users-available-sort-nome">Nome Usuário <span class="ua-sort-icon">↕</span></th>
|
||||
<th>E-mail</th>
|
||||
</tr>
|
||||
<tr class="pa-users-table__filters">
|
||||
<th></th>
|
||||
<th><input type="text" id="pa-users-available-search-nome" placeholder="Buscar por nome..." autocomplete="off" /></th>
|
||||
<th><input type="text" id="pa-users-available-search-email" placeholder="Buscar por e-mail..." autocomplete="off" /></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="pa-users-available-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="pa-footer" id="pa-users-available-footer"></div>
|
||||
</div>
|
||||
|
||||
<div class="pa-users-panel">
|
||||
<div class="pa-users-panel__header">
|
||||
<button type="button" class="icon-btn" id="pa-users-linked-refresh-btn" aria-label="Atualizar lista">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 4v6h-6M1 20v-6h6" stroke-linecap="round" stroke-linejoin="round"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
</button>
|
||||
<button type="button" class="btn-danger-outline" id="pa-users-unlink-btn" disabled>Desvincular</button>
|
||||
</div>
|
||||
<div class="pa-table-wrap">
|
||||
<table class="pa-table pa-users-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="pa-users-table__checkbox"><input type="checkbox" id="pa-users-linked-select-all" aria-label="Selecionar todos" /></th>
|
||||
<th class="pa-users-table__sort" id="pa-users-linked-sort-nome">Nome Usuário <span class="ua-sort-icon">↕</span></th>
|
||||
<th>E-mail</th>
|
||||
</tr>
|
||||
<tr class="pa-users-table__filters">
|
||||
<th></th>
|
||||
<th><input type="text" id="pa-users-linked-search-nome" placeholder="Buscar por nome..." autocomplete="off" /></th>
|
||||
<th><input type="text" id="pa-users-linked-search-email" placeholder="Buscar por e-mail..." autocomplete="off" /></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="pa-users-linked-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="pa-footer" id="pa-users-linked-footer"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pa-user-list" id="pa-user-list"></div>
|
||||
</div>
|
||||
|
||||
<div class="pa-edit-actions">
|
||||
@ -346,19 +396,62 @@
|
||||
|
||||
|
||||
<div class="modal-overlay" id="manage-account-modal" hidden>
|
||||
<div class="modal-card">
|
||||
<div class="modal-card" id="manage-account-modal-card">
|
||||
<h2 class="modal-card__title">Gerenciar Usuário</h2>
|
||||
<div class="modal-field">
|
||||
<button type="button" class="btn-outline" id="manage-account-change-password-btn">Alterar senha</button>
|
||||
</div>
|
||||
<div class="modal-field" id="manage-account-liderados-field" hidden>
|
||||
<label>Usuários sob sua liderança</label>
|
||||
<input type="text" id="manage-account-liderados-search" class="checklist-search" placeholder="Buscar por nome ou departamento..." autocomplete="off" />
|
||||
<label class="modal-checkbox checklist-select-all">
|
||||
<input type="checkbox" id="manage-account-liderados-select-all" />
|
||||
Marcar todos os resultados da busca
|
||||
</label>
|
||||
<div class="checklist-box" id="manage-account-liderados-checklist"></div>
|
||||
<div class="dual-select">
|
||||
<div class="dual-select__panel">
|
||||
<div class="dual-select__panel-header">
|
||||
<button type="button" class="btn-solid" id="manage-account-liderados-available-link-btn" disabled>Vincular</button>
|
||||
</div>
|
||||
<div class="dual-select__table-wrap">
|
||||
<table class="dual-select__table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" id="manage-account-liderados-available-select-all" aria-label="Selecionar todos" /></th>
|
||||
<th class="dual-select__sort" id="manage-account-liderados-available-sort-nome">Nome <span class="dual-select__sort-icon">↕</span></th>
|
||||
<th>Departamento</th>
|
||||
</tr>
|
||||
<tr class="dual-select__filters">
|
||||
<th></th>
|
||||
<th><input type="text" id="manage-account-liderados-available-search-nome" placeholder="Buscar por nome..." autocomplete="off" /></th>
|
||||
<th><input type="text" id="manage-account-liderados-available-search-departamento" placeholder="Buscar por departamento..." autocomplete="off" /></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="manage-account-liderados-available-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="dual-select__footer" id="manage-account-liderados-available-footer"></div>
|
||||
</div>
|
||||
|
||||
<div class="dual-select__panel">
|
||||
<div class="dual-select__panel-header">
|
||||
<button type="button" class="btn-danger-outline" id="manage-account-liderados-linked-unlink-btn" disabled>Desvincular</button>
|
||||
</div>
|
||||
<div class="dual-select__table-wrap">
|
||||
<table class="dual-select__table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" id="manage-account-liderados-linked-select-all" aria-label="Selecionar todos" /></th>
|
||||
<th class="dual-select__sort" id="manage-account-liderados-linked-sort-nome">Nome <span class="dual-select__sort-icon">↕</span></th>
|
||||
<th>Departamento</th>
|
||||
</tr>
|
||||
<tr class="dual-select__filters">
|
||||
<th></th>
|
||||
<th><input type="text" id="manage-account-liderados-linked-search-nome" placeholder="Buscar por nome..." autocomplete="off" /></th>
|
||||
<th><input type="text" id="manage-account-liderados-linked-search-departamento" placeholder="Buscar por departamento..." autocomplete="off" /></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="manage-account-liderados-linked-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="dual-select__footer" id="manage-account-liderados-linked-footer"></div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="modal-error" id="manage-account-liderados-error"></p>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
@ -455,6 +548,7 @@
|
||||
</div>
|
||||
|
||||
<script src="{% static 'js/api.js' %}"></script>
|
||||
<script src="{% static 'js/dual-select.js' %}"></script>
|
||||
<script src="{% static 'js/profiles.js' %}"></script>
|
||||
<script src="{% static 'js/auth.js' %}"></script>
|
||||
<script src="{% static 'js/access.js' %}"></script>
|
||||
|
||||
@ -360,19 +360,62 @@
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="manage-account-modal" hidden>
|
||||
<div class="modal-card">
|
||||
<div class="modal-card" id="manage-account-modal-card">
|
||||
<h2 class="modal-card__title">Gerenciar Usuário</h2>
|
||||
<div class="modal-field">
|
||||
<button type="button" class="btn-outline" id="manage-account-change-password-btn">Alterar senha</button>
|
||||
</div>
|
||||
<div class="modal-field" id="manage-account-liderados-field" hidden>
|
||||
<label>Usuários sob sua liderança</label>
|
||||
<input type="text" id="manage-account-liderados-search" class="checklist-search" placeholder="Buscar por nome ou departamento..." autocomplete="off" />
|
||||
<label class="modal-checkbox checklist-select-all">
|
||||
<input type="checkbox" id="manage-account-liderados-select-all" />
|
||||
Marcar todos os resultados da busca
|
||||
</label>
|
||||
<div class="checklist-box" id="manage-account-liderados-checklist"></div>
|
||||
<div class="dual-select">
|
||||
<div class="dual-select__panel">
|
||||
<div class="dual-select__panel-header">
|
||||
<button type="button" class="btn-solid" id="manage-account-liderados-available-link-btn" disabled>Vincular</button>
|
||||
</div>
|
||||
<div class="dual-select__table-wrap">
|
||||
<table class="dual-select__table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" id="manage-account-liderados-available-select-all" aria-label="Selecionar todos" /></th>
|
||||
<th class="dual-select__sort" id="manage-account-liderados-available-sort-nome">Nome <span class="dual-select__sort-icon">↕</span></th>
|
||||
<th>Departamento</th>
|
||||
</tr>
|
||||
<tr class="dual-select__filters">
|
||||
<th></th>
|
||||
<th><input type="text" id="manage-account-liderados-available-search-nome" placeholder="Buscar por nome..." autocomplete="off" /></th>
|
||||
<th><input type="text" id="manage-account-liderados-available-search-departamento" placeholder="Buscar por departamento..." autocomplete="off" /></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="manage-account-liderados-available-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="dual-select__footer" id="manage-account-liderados-available-footer"></div>
|
||||
</div>
|
||||
|
||||
<div class="dual-select__panel">
|
||||
<div class="dual-select__panel-header">
|
||||
<button type="button" class="btn-danger-outline" id="manage-account-liderados-linked-unlink-btn" disabled>Desvincular</button>
|
||||
</div>
|
||||
<div class="dual-select__table-wrap">
|
||||
<table class="dual-select__table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" id="manage-account-liderados-linked-select-all" aria-label="Selecionar todos" /></th>
|
||||
<th class="dual-select__sort" id="manage-account-liderados-linked-sort-nome">Nome <span class="dual-select__sort-icon">↕</span></th>
|
||||
<th>Departamento</th>
|
||||
</tr>
|
||||
<tr class="dual-select__filters">
|
||||
<th></th>
|
||||
<th><input type="text" id="manage-account-liderados-linked-search-nome" placeholder="Buscar por nome..." autocomplete="off" /></th>
|
||||
<th><input type="text" id="manage-account-liderados-linked-search-departamento" placeholder="Buscar por departamento..." autocomplete="off" /></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="manage-account-liderados-linked-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="dual-select__footer" id="manage-account-liderados-linked-footer"></div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="modal-error" id="manage-account-liderados-error"></p>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
@ -437,6 +480,7 @@
|
||||
</div>
|
||||
|
||||
<script src="{% static 'js/api.js' %}"></script>
|
||||
<script src="{% static 'js/dual-select.js' %}"></script>
|
||||
<script src="{% static 'js/profiles.js' %}"></script>
|
||||
<script src="{% static 'js/auth.js' %}"></script>
|
||||
<script src="{% static 'js/access.js' %}"></script>
|
||||
|
||||
@ -578,19 +578,62 @@
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="manage-account-modal" hidden>
|
||||
<div class="modal-card">
|
||||
<div class="modal-card" id="manage-account-modal-card">
|
||||
<h2 class="modal-card__title">Gerenciar Usuário</h2>
|
||||
<div class="modal-field">
|
||||
<button type="button" class="btn-outline" id="manage-account-change-password-btn">Alterar senha</button>
|
||||
</div>
|
||||
<div class="modal-field" id="manage-account-liderados-field" hidden>
|
||||
<label>Usuários sob sua liderança</label>
|
||||
<input type="text" id="manage-account-liderados-search" class="checklist-search" placeholder="Buscar por nome ou departamento..." autocomplete="off" />
|
||||
<label class="modal-checkbox checklist-select-all">
|
||||
<input type="checkbox" id="manage-account-liderados-select-all" />
|
||||
Marcar todos os resultados da busca
|
||||
</label>
|
||||
<div class="checklist-box" id="manage-account-liderados-checklist"></div>
|
||||
<div class="dual-select">
|
||||
<div class="dual-select__panel">
|
||||
<div class="dual-select__panel-header">
|
||||
<button type="button" class="btn-solid" id="manage-account-liderados-available-link-btn" disabled>Vincular</button>
|
||||
</div>
|
||||
<div class="dual-select__table-wrap">
|
||||
<table class="dual-select__table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" id="manage-account-liderados-available-select-all" aria-label="Selecionar todos" /></th>
|
||||
<th class="dual-select__sort" id="manage-account-liderados-available-sort-nome">Nome <span class="dual-select__sort-icon">↕</span></th>
|
||||
<th>Departamento</th>
|
||||
</tr>
|
||||
<tr class="dual-select__filters">
|
||||
<th></th>
|
||||
<th><input type="text" id="manage-account-liderados-available-search-nome" placeholder="Buscar por nome..." autocomplete="off" /></th>
|
||||
<th><input type="text" id="manage-account-liderados-available-search-departamento" placeholder="Buscar por departamento..." autocomplete="off" /></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="manage-account-liderados-available-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="dual-select__footer" id="manage-account-liderados-available-footer"></div>
|
||||
</div>
|
||||
|
||||
<div class="dual-select__panel">
|
||||
<div class="dual-select__panel-header">
|
||||
<button type="button" class="btn-danger-outline" id="manage-account-liderados-linked-unlink-btn" disabled>Desvincular</button>
|
||||
</div>
|
||||
<div class="dual-select__table-wrap">
|
||||
<table class="dual-select__table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" id="manage-account-liderados-linked-select-all" aria-label="Selecionar todos" /></th>
|
||||
<th class="dual-select__sort" id="manage-account-liderados-linked-sort-nome">Nome <span class="dual-select__sort-icon">↕</span></th>
|
||||
<th>Departamento</th>
|
||||
</tr>
|
||||
<tr class="dual-select__filters">
|
||||
<th></th>
|
||||
<th><input type="text" id="manage-account-liderados-linked-search-nome" placeholder="Buscar por nome..." autocomplete="off" /></th>
|
||||
<th><input type="text" id="manage-account-liderados-linked-search-departamento" placeholder="Buscar por departamento..." autocomplete="off" /></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="manage-account-liderados-linked-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="dual-select__footer" id="manage-account-liderados-linked-footer"></div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="modal-error" id="manage-account-liderados-error"></p>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
@ -655,6 +698,7 @@
|
||||
</div>
|
||||
|
||||
<script src="{% static 'js/api.js' %}"></script>
|
||||
<script src="{% static 'js/dual-select.js' %}"></script>
|
||||
<script src="{% static 'js/profiles.js' %}"></script>
|
||||
<script src="{% static 'js/auth.js' %}"></script>
|
||||
<script src="{% static 'js/access.js' %}"></script>
|
||||
|
||||
@ -275,13 +275,18 @@
|
||||
</div>
|
||||
|
||||
<div class="pa-table-wrap">
|
||||
<table class="pa-table">
|
||||
<table class="pa-table" id="ua-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:160px;">Login</th>
|
||||
<th>Nome</th>
|
||||
<th style="width:220px;">Perfil de Acesso</th>
|
||||
<th style="width:100px;">Status</th>
|
||||
<th data-sort="username" style="width:130px;">Login <span class="ua-sort-icon">↕</span></th>
|
||||
<th data-sort="nome">Nome <span class="ua-sort-icon">↕</span></th>
|
||||
<th data-sort="codigo_folha" style="width:110px;">Cód. Folha <span class="ua-sort-icon">↕</span></th>
|
||||
<th data-sort="codigo_questor" style="width:120px;">Cód. Questor <span class="ua-sort-icon">↕</span></th>
|
||||
<th data-sort="codigo_tareffa" style="width:120px;">Cód. Tareffa <span class="ua-sort-icon">↕</span></th>
|
||||
<th data-sort="codigo_contabit" style="width:120px;">Cód. Contabit <span class="ua-sort-icon">↕</span></th>
|
||||
<th data-sort="ramal" style="width:90px;">Ramal <span class="ua-sort-icon">↕</span></th>
|
||||
<th style="width:180px;">Perfil de Acesso</th>
|
||||
<th data-sort="status" style="width:100px;">Status <span class="ua-sort-icon">↕</span></th>
|
||||
<th style="width:130px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -329,6 +334,58 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ua-section">
|
||||
<h3 class="ua-section-title">Dados Cadastrais</h3>
|
||||
<div class="pa-field-row pa-field-row--auto">
|
||||
<div class="pa-field">
|
||||
<label for="ua-email">E-mail</label>
|
||||
<input type="email" id="ua-email" placeholder="Ex.: maria.silva@depaulacontadores.com.br" autocomplete="off" />
|
||||
</div>
|
||||
<div class="pa-field">
|
||||
<label for="ua-codigo-folha">Código da Folha</label>
|
||||
<div class="ua-field-link">
|
||||
<input type="text" id="ua-codigo-folha" placeholder="Ex.: 567/327" autocomplete="off" />
|
||||
<button type="button" class="ua-vincular-btn" disabled title="Vinculação com sistema externo ainda não implementada">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 17H7a5 5 0 0 1 0-10h2" stroke-linecap="round"/><path d="M15 7h2a5 5 0 0 1 0 10h-2" stroke-linecap="round"/><path d="M8 12h8" stroke-linecap="round"/></svg>
|
||||
Vincular
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pa-field">
|
||||
<label for="ua-codigo-questor">Código do Questor</label>
|
||||
<div class="ua-field-link">
|
||||
<input type="text" id="ua-codigo-questor" placeholder="Não cadastrado" autocomplete="off" />
|
||||
<button type="button" class="ua-vincular-btn" disabled title="Vinculação com sistema externo ainda não implementada">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 17H7a5 5 0 0 1 0-10h2" stroke-linecap="round"/><path d="M15 7h2a5 5 0 0 1 0 10h-2" stroke-linecap="round"/><path d="M8 12h8" stroke-linecap="round"/></svg>
|
||||
Vincular
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pa-field">
|
||||
<label for="ua-codigo-tareffa">Código do Tareffa</label>
|
||||
<div class="ua-field-link">
|
||||
<input type="text" id="ua-codigo-tareffa" placeholder="Não cadastrado" autocomplete="off" />
|
||||
<button type="button" class="ua-vincular-btn" disabled title="Vinculação com sistema externo ainda não implementada">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 17H7a5 5 0 0 1 0-10h2" stroke-linecap="round"/><path d="M15 7h2a5 5 0 0 1 0 10h-2" stroke-linecap="round"/><path d="M8 12h8" stroke-linecap="round"/></svg>
|
||||
Vincular
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pa-field">
|
||||
<label for="ua-codigo-contabit">Código do Contabit</label>
|
||||
<input type="text" id="ua-codigo-contabit" placeholder="Não cadastrado" autocomplete="off" />
|
||||
</div>
|
||||
<div class="pa-field">
|
||||
<label for="ua-ramal">Ramal</label>
|
||||
<input type="text" id="ua-ramal" placeholder="Não cadastrado" autocomplete="off" />
|
||||
</div>
|
||||
<div class="pa-field">
|
||||
<label for="ua-data-aniversario">Data de Aniversário</label>
|
||||
<input type="date" id="ua-data-aniversario" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ua-section">
|
||||
<h3 class="ua-section-title">Permissões</h3>
|
||||
<div class="pa-field-row ua-permissions-row">
|
||||
@ -356,45 +413,54 @@
|
||||
</label>
|
||||
<div class="pa-field ua-liderados-field" id="ua-liderados-field" hidden>
|
||||
<label>Usuários sob liderança</label>
|
||||
<input type="text" id="ua-liderados-search" class="checklist-search" placeholder="Buscar por nome ou departamento..." autocomplete="off" />
|
||||
<label class="modal-checkbox checklist-select-all">
|
||||
<input type="checkbox" id="ua-liderados-select-all" />
|
||||
Marcar todos os resultados da busca
|
||||
</label>
|
||||
<div class="checklist-box" id="ua-liderados-checklist"></div>
|
||||
<div class="dual-select">
|
||||
<div class="dual-select__panel">
|
||||
<div class="dual-select__panel-header">
|
||||
<button type="button" class="btn-solid" id="ua-liderados-available-link-btn" disabled>Vincular</button>
|
||||
</div>
|
||||
<div class="dual-select__table-wrap">
|
||||
<table class="dual-select__table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" id="ua-liderados-available-select-all" aria-label="Selecionar todos" /></th>
|
||||
<th class="dual-select__sort" id="ua-liderados-available-sort-nome">Nome <span class="dual-select__sort-icon">↕</span></th>
|
||||
<th>Departamento</th>
|
||||
</tr>
|
||||
<tr class="dual-select__filters">
|
||||
<th></th>
|
||||
<th><input type="text" id="ua-liderados-available-search-nome" placeholder="Buscar por nome..." autocomplete="off" /></th>
|
||||
<th><input type="text" id="ua-liderados-available-search-departamento" placeholder="Buscar por departamento..." autocomplete="off" /></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="ua-liderados-available-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="dual-select__footer" id="ua-liderados-available-footer"></div>
|
||||
</div>
|
||||
|
||||
<div class="ua-section">
|
||||
<h3 class="ua-section-title">Dados Cadastrais</h3>
|
||||
<div class="pa-field-row pa-field-row--auto">
|
||||
<div class="pa-field">
|
||||
<label for="ua-email">E-mail</label>
|
||||
<input type="email" id="ua-email" placeholder="Ex.: maria.silva@depaulacontadores.com.br" autocomplete="off" />
|
||||
<div class="dual-select__panel">
|
||||
<div class="dual-select__panel-header">
|
||||
<button type="button" class="btn-danger-outline" id="ua-liderados-linked-unlink-btn" disabled>Desvincular</button>
|
||||
</div>
|
||||
<div class="pa-field">
|
||||
<label for="ua-codigo-folha">Código da Folha</label>
|
||||
<input type="text" id="ua-codigo-folha" placeholder="Ex.: 567/327" autocomplete="off" />
|
||||
<div class="dual-select__table-wrap">
|
||||
<table class="dual-select__table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" id="ua-liderados-linked-select-all" aria-label="Selecionar todos" /></th>
|
||||
<th class="dual-select__sort" id="ua-liderados-linked-sort-nome">Nome <span class="dual-select__sort-icon">↕</span></th>
|
||||
<th>Departamento</th>
|
||||
</tr>
|
||||
<tr class="dual-select__filters">
|
||||
<th></th>
|
||||
<th><input type="text" id="ua-liderados-linked-search-nome" placeholder="Buscar por nome..." autocomplete="off" /></th>
|
||||
<th><input type="text" id="ua-liderados-linked-search-departamento" placeholder="Buscar por departamento..." autocomplete="off" /></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="ua-liderados-linked-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="pa-field">
|
||||
<label for="ua-codigo-questor">Código do Questor</label>
|
||||
<input type="text" id="ua-codigo-questor" placeholder="Não cadastrado" autocomplete="off" />
|
||||
<div class="dual-select__footer" id="ua-liderados-linked-footer"></div>
|
||||
</div>
|
||||
<div class="pa-field">
|
||||
<label for="ua-codigo-tareffa">Código do Tareffa</label>
|
||||
<input type="text" id="ua-codigo-tareffa" placeholder="Não cadastrado" autocomplete="off" />
|
||||
</div>
|
||||
<div class="pa-field">
|
||||
<label for="ua-codigo-contabit">Código do Contabit</label>
|
||||
<input type="text" id="ua-codigo-contabit" placeholder="Não cadastrado" autocomplete="off" />
|
||||
</div>
|
||||
<div class="pa-field">
|
||||
<label for="ua-ramal">Ramal</label>
|
||||
<input type="text" id="ua-ramal" placeholder="Não cadastrado" autocomplete="off" />
|
||||
</div>
|
||||
<div class="pa-field">
|
||||
<label for="ua-data-aniversario">Data de Aniversário</label>
|
||||
<input type="date" id="ua-data-aniversario" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -413,19 +479,62 @@
|
||||
|
||||
|
||||
<div class="modal-overlay" id="manage-account-modal" hidden>
|
||||
<div class="modal-card">
|
||||
<div class="modal-card" id="manage-account-modal-card">
|
||||
<h2 class="modal-card__title">Gerenciar Usuário</h2>
|
||||
<div class="modal-field">
|
||||
<button type="button" class="btn-outline" id="manage-account-change-password-btn">Alterar senha</button>
|
||||
</div>
|
||||
<div class="modal-field" id="manage-account-liderados-field" hidden>
|
||||
<label>Usuários sob sua liderança</label>
|
||||
<input type="text" id="manage-account-liderados-search" class="checklist-search" placeholder="Buscar por nome ou departamento..." autocomplete="off" />
|
||||
<label class="modal-checkbox checklist-select-all">
|
||||
<input type="checkbox" id="manage-account-liderados-select-all" />
|
||||
Marcar todos os resultados da busca
|
||||
</label>
|
||||
<div class="checklist-box" id="manage-account-liderados-checklist"></div>
|
||||
<div class="dual-select">
|
||||
<div class="dual-select__panel">
|
||||
<div class="dual-select__panel-header">
|
||||
<button type="button" class="btn-solid" id="manage-account-liderados-available-link-btn" disabled>Vincular</button>
|
||||
</div>
|
||||
<div class="dual-select__table-wrap">
|
||||
<table class="dual-select__table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" id="manage-account-liderados-available-select-all" aria-label="Selecionar todos" /></th>
|
||||
<th class="dual-select__sort" id="manage-account-liderados-available-sort-nome">Nome <span class="dual-select__sort-icon">↕</span></th>
|
||||
<th>Departamento</th>
|
||||
</tr>
|
||||
<tr class="dual-select__filters">
|
||||
<th></th>
|
||||
<th><input type="text" id="manage-account-liderados-available-search-nome" placeholder="Buscar por nome..." autocomplete="off" /></th>
|
||||
<th><input type="text" id="manage-account-liderados-available-search-departamento" placeholder="Buscar por departamento..." autocomplete="off" /></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="manage-account-liderados-available-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="dual-select__footer" id="manage-account-liderados-available-footer"></div>
|
||||
</div>
|
||||
|
||||
<div class="dual-select__panel">
|
||||
<div class="dual-select__panel-header">
|
||||
<button type="button" class="btn-danger-outline" id="manage-account-liderados-linked-unlink-btn" disabled>Desvincular</button>
|
||||
</div>
|
||||
<div class="dual-select__table-wrap">
|
||||
<table class="dual-select__table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" id="manage-account-liderados-linked-select-all" aria-label="Selecionar todos" /></th>
|
||||
<th class="dual-select__sort" id="manage-account-liderados-linked-sort-nome">Nome <span class="dual-select__sort-icon">↕</span></th>
|
||||
<th>Departamento</th>
|
||||
</tr>
|
||||
<tr class="dual-select__filters">
|
||||
<th></th>
|
||||
<th><input type="text" id="manage-account-liderados-linked-search-nome" placeholder="Buscar por nome..." autocomplete="off" /></th>
|
||||
<th><input type="text" id="manage-account-liderados-linked-search-departamento" placeholder="Buscar por departamento..." autocomplete="off" /></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="manage-account-liderados-linked-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="dual-select__footer" id="manage-account-liderados-linked-footer"></div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="modal-error" id="manage-account-liderados-error"></p>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
@ -490,6 +599,7 @@
|
||||
</div>
|
||||
|
||||
<script src="{% static 'js/api.js' %}"></script>
|
||||
<script src="{% static 'js/dual-select.js' %}"></script>
|
||||
<script src="{% static 'js/profiles.js' %}"></script>
|
||||
<script src="{% static 'js/auth.js' %}"></script>
|
||||
<script src="{% static 'js/access.js' %}"></script>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user