85 lines
17 KiB
Markdown
85 lines
17 KiB
Markdown
# Ramais
|
|
|
|
> Movido do `CLAUDE.md` da raiz em 2026-08-26 para reduzir conflito de edição entre aplicações. Ver `CLAUDE.md` na raiz para arquitetura geral/transversal do Portal (modelo de permissões, API, CSS, etc.). Este arquivo não é auto-carregado pelo Claude Code (não há pacote Python dedicado a Ramais, o código vive em `portal_api/models.py`/`views.py`/`serializers.py` junto com o resto) — leia manualmente ao mexer nesta aplicação.
|
|
|
|
O diretório de `ramais.html` é **automático**: `RamalViewSet.list()` (não o `RamalSerializer` — esse serializer só cobre as linhas avulsas via CRUD normal) mescla, a cada `GET /api/ramais/`, duas fontes numa lista só, ordenada por nome:
|
|
|
|
1. Todo `Usuario` ativo — a linha é montada direto do cadastro (`nome`, `Usuario.departamentos` juntados por vírgula, `Usuario.ramal`); se o colaborador ainda não tem ramal preenchido, `numero_exibicao` vem vazio e o frontend mostra um badge "Pendente" em vez de reclamar de campo faltando.
|
|
2. As linhas avulsas de `Ramal` (sem `Usuario` por trás — telefone de sala, recepção etc.), cadastradas pelo modal "Adicionar Ramal".
|
|
|
|
Cada item da lista mesclada tem um `id` sintético (`"usuario-<id>"` ou `"avulso-<id>"`) e um campo `tipo` (`"usuario"`/`"avulso"`) que o frontend usa pra decidir qual endpoint chamar ao editar/excluir — não existe mais um model unificando os dois casos com uma FK opcional (essa foi a primeira versão da tela; revertida a pedido do usuário pra eliminar o passo manual de "adicionar" alguém que já tem cadastro).
|
|
|
|
Segue o mesmo padrão visualizar/editar de Links & Ferramentas (ver `CLAUDE.md` na raiz, seção "Padrão visualizar/editar" dentro de "Modelo de permissões"): leitura exige `apps.visualizar` (liberado a todo perfil, já que `ramais` está em `BASE_KEYS`), escrita exige `apps.editar` — por ora só `True` pra "Integração e Inovação" no `seed_portal.py`, exatamente como pedido; liberar outro perfil não pede código novo, só marcar o app na árvore de Perfis de Acesso.
|
|
|
|
- **Editar o ramal de um colaborador de verdade**: não existe "criar" — a linha já aparece sozinha. O lápis na linha abre o mesmo modal de Ramal, mas com Nome/Departamento desabilitados (só leitura do cadastro) e só o campo Ramal editável; salvar chama `PATCH /api/ramais/usuarios/{usuario_id}/` (`RamalViewSet.atualizar_ramal_usuario`), que grava direto em `Usuario.ramal` — é assim que a tela demonstra a alteração refletindo no cadastro do usuário.
|
|
- **Linha avulsa**: "Adicionar Ramal" sempre cria uma linha avulsa (`POST /api/ramais/`, `nome`/`departamento`/`numero` livres — só `nome` é obrigatório, o resto pode ficar pendente igual a um colaborador de verdade). Editar/excluir uma linha avulsa usa `PATCH`/`DELETE /api/ramais/{avulso_id}/` normalmente; excluir só existe pra esse tipo (não dá pra "excluir" um colaborador daqui — isso é na tela de Usuários).
|
|
- **Lista de usuários do modal de Ausência**: `RamalViewSet.usuarios_disponiveis` (`GET /api/ramais/usuarios/`) devolve só `id`/`nome` de usuários ativos, pra alimentar o `<select>` "Lista de Usuários" do modal "Criar Ausência" (o único modal que ainda precisa escolher uma pessoa numa lista — o modal de Ramal não precisa mais, já que a linha do colaborador já existe). Não reaproveita `/api/usuarios/` de propósito — aquele endpoint é restrito a `gerencia_permissoes`, e a permissão de Ramais é deliberadamente desacoplada disso (hoje dá na mesma pessoa, mas não presume que sempre será assim).
|
|
- **Ausência** (`RamalAusencia`): um registro por período criado pelo modal "Criar Ausência"; "ausente agora" nunca é armazenado — `RamalAusencia.esta_ativa()` compara a hora atual (`timezone.localtime()`) contra `[data_inicio+hora_inicio, data_fim+hora_volta]` (hora ausente = considera o dia inteiro) toda vez que `RamalViewSet.list()` monta a resposta. Clicar no badge "(AUSENTE)" de uma linha (`data-ram-ver-ausencia`, qualquer um com `apps.visualizar` pode abrir) faz `GET /api/ramais-ausencias/{id}/` e abre o modal "Visualizar Ausência" — campos desabilitados (`<input type="date"/"time">` mostra a data/hora formatada mesmo `disabled`, sem precisar formatar manualmente), com "Editar Ausência"/"Deletar Ausência" visíveis só pra quem tem `apps.editar`. "Editar" habilita os mesmos campos in-place (sem modal novo) e troca os botões por "Cancelar"/"Salvar Ausência" (`PATCH /api/ramais-ausencias/{id}/`); "Deletar" remove o registro (`DELETE`) — não existe mais um botão de "encerrar antes do previsto" separado (a rodada anterior tinha isso via `encerrada_manualmente`; substituído por editar/excluir de verdade, mais simples e é o que foi pedido). O campo `encerrada_manualmente` continua no model (histórico/uso futuro via admin), só não tem mais UI própria.
|
|
- **Aniversariante**: comparação de `Usuario.data_aniversario` (mês/dia) com `timezone.localdate()`, feita no mesmo `list()` — mesmo campo que já existia no cadastro de Usuários, sem nada novo ali.
|
|
- **Selos de ausente/aniversariante**: `.ram-badge--ausente`/`.ram-badge--aniversario` (`ramais.css`) são selos (pill) com cor de texto/fundo ajustada por tema via `:root[data-theme="light"] .ram-badge--*` — não reaproveitam `--danger`/`--gold` crus porque esses tokens não foram pensados pra texto pequeno sobre um selo (contraste insuficiente). A linha inteira também é tingida (`.ram-row--ausente`/`.ram-row--aniversario` td, aplicado via classe no `<tr>` em `ramais.js`) com a mesma cor do selo, também ajustada por tema — pedido explícito do usuário pra facilitar notar a linha antes mesmo de ler o selo (a versão anterior sem tingimento de linha foi revertida).
|
|
- **Férias**: a aba existe (navegação por abas, ver abaixo) mas está **vazia de propósito** — o conteúdo foi adiado pra uma rodada futura; a limitação original ("depende de integração futura com outro banco") continua valendo, só a decisão de já reservar o espaço na navegação é nova.
|
|
- **Novo Chamado**: botão que abre um modal com um `<iframe>` apontando para a ferramenta externa de chamados (`https://depaula-tvcorporativa.lovable.app/chamar?token=...`) — decisão explícita de ficar embutido na própria tela em vez de nova aba (diferente do padrão dos demais links externos do portal). O `src` do iframe só é setado na abertura do modal e volta pra `about:blank` ao fechar, pra não deixar a ferramenta carregada em segundo plano.
|
|
- **Sem reordenação**: ao contrário de Links & Ferramentas/Widgets, a listagem é sempre alfabética (`sort()` em `list()`), sem `ordem`/drag-and-drop.
|
|
- Usuário inativo (`is_active=False`) não aparece mais no diretório (o `list()` filtra `Usuario.objects.filter(is_active=True)`) — diferença deliberada da primeira versão, que ainda mostrava inativos se tivessem uma linha vinculada.
|
|
|
|
## Navegação por abas em `ramais.html` (subtelas)
|
|
|
|
`ramais.html` deixou de ser uma tela única — é uma seção com 5 subtelas, navegáveis por abas logo abaixo do cabeçalho: **Ramais** (diretório descrito acima, ativa por padrão), **Responsável no Tareffa** (placeholder vazio), **Telefones Externos**, **Férias** (placeholder vazio) e **Funções de Telefonia**. As abas reaproveitam o CSS genérico `.pa-tabs`/`.pa-tab`/`.pa-tab-panel` (`perfis-acesso.css`, já carregado nesta página desde antes — mesmo padrão usado nas abas Permissões/Usuários de `perfis-acesso.html`), mas com atributos próprios (`data-ram-tab`/`data-ram-tab-panel`) e uma implementação independente em `ramais.js` (`activeRamTab`/`renderRamTabs()`), pra não colidir com `profiles.js`. Os botões "Adicionar Ramal"/"Novo Chamado"/"Criar Ausência" continuam só dentro do painel "Ramais" — cada subtela tem suas próprias ações.
|
|
|
|
**Permissão — uma dupla visualizar/(editar) por subtela**: cada uma das 5 abas tem sua própria permissão de visualização, e as 3 com conteúdo administrável (Ramais, Telefones Externos, Funções de Telefonia) também têm sua própria permissão de edição — não é mais um único par genérico `ramais.apps.visualizar`/`ramais.apps.editar` cobrindo tudo (esse desenho, usado na primeira versão da navegação por abas, foi revisto no mesmo dia a pedido do usuário: "deve haver permissão de visualização para cada um dos itens e edição para as de ramais, telefone externos e funções de telefonia"). Em `catalogo.MODULE_APPS["ramais"]`, isso é modelado como **5 subgrupos** (mesmo formato `{"key", "label", "tools": [...]}` já usado em Auditorias — reaproveita 100% a árvore de permissões genérica de `profiles.js`, sem UI nova):
|
|
|
|
```python
|
|
"ramais": [
|
|
{"key": "ramais-diretorio", "label": "Ramais", "tools": [
|
|
{"key": "ramais-visualizar", "label": "Visualizar"},
|
|
{"key": "ramais-editar", "label": "Editar (...)"},
|
|
]},
|
|
{"key": "responsavel-tareffa", "label": "Responsável no Tareffa", "tools": [
|
|
{"key": "responsavel-tareffa-visualizar", "label": "Visualizar"},
|
|
]},
|
|
{"key": "telefones-externos", "label": "Telefones Externos", "tools": [...]},
|
|
{"key": "ferias", "label": "Férias", "tools": [{"key": "ferias-visualizar", ...}]},
|
|
{"key": "funcoes-telefonia", "label": "Funções de Telefonia", "tools": [...]},
|
|
],
|
|
```
|
|
|
|
Cada `ModelViewSet` (`RamalViewSet`/`RamalAusenciaViewSet`, `TelefoneExternoViewSet`, `FuncaoTelefoniaViewSet`) instancia `PermissaoApp("ramais", app_key)` com a chave da própria subtela (ex.: `"telefones-externos-visualizar"`/`"telefones-externos-editar"`) — `RamalAusenciaViewSet` usa as mesmas chaves `ramais-visualizar`/`ramais-editar` do diretório de Ramais, já que ausência é parte dessa subtela, não uma quinta. No frontend, `ramais.js` calcula um `canView`/`canManage` por subtela a partir de `me.permissoes_efetivas.ramais.apps[chave]`, esconde (`hidden`) o botão de cada aba cujo `visualizar` for falso, e escolhe a primeira aba visível como ativa por padrão (em vez de sempre abrir em "Ramais", que pode estar oculta pra esse perfil). `ramais-lookup.js` (modal de consulta rápida no topbar) usa especificamente `ramais-visualizar`, já que só mostra o diretório de Ramais, não as outras subtelas.
|
|
|
|
**Cuidado com `seed_portal.py`** (mesmo princípio da nota geral em "Padrão visualizar/editar", ver `CLAUDE.md` na raiz): como `ramais` está em `BASE_KEYS`, `permissions_from_keys()` habilitaria os 8 apps (visualizar de todas as 5 + editar das 3) de uma vez — sem o override, todo perfil nasceria podendo editar. Por isso `seed_portal.py` força `ramais-editar`/`telefones-externos-editar`/`funcoes-telefonia-editar` para `False` explicitamente em todo perfil que não seja "Integração e Inovação", depois de montar o dict — os `*-visualizar` ficam `True` pra todo mundo de propósito ("os demais terão acesso para visualizar todas"). Qualquer mudança de nome/adição de subtela nesse padrão precisa replicar esse mesmo cuidado.
|
|
|
|
Um perfil só-visualizar vê as 5 abas e as tabelas, mas nunca os botões de Adicionar/editar/excluir em nenhuma delas; um perfil sem `visualizar` numa subtela específica não vê nem a aba dela.
|
|
|
|
**Telefones Externos** (`TelefoneExterno`, model dedicado sem FK — contatos de fornecedores/terceiros, não de `Usuario`): CRUD simples via `/api/telefones-externos/`, só `nome` obrigatório (`ramal`/`telefone`/`observacoes` opcionais, mesmo padrão de `Ramal` avulso). Dois filtros de busca (`ram-tel-search-nome`/`ram-tel-search-obs`, client-side sobre o array já carregado) — por nome e por observações, ao mesmo tempo, sem OR/AND configurável. A tabela começa vazia (nenhum seed) — o usuário cadastra pela própria tela.
|
|
|
|
**Funções de Telefonia** (`FuncaoTelefonia`) — comandos padrão da central telefônica (ex.: `*01 + Código de Agente` → LogOn). CRUD via `/api/funcoes-telefonia/`, só `comando` obrigatório. `Meta.ordering = ["comando"]` reproduz sozinho a ordem esperada (`*0, *01, ..., *5, *503, *8`) porque os códigos já nascem em ordem lexicográfica como string — não precisou de um campo `ordem` manual nem de endpoint de reorder, ao contrário de `LinkFerramenta`/`Favorito`/`WidgetUsuario`. Ao contrário de Telefones Externos, esta tabela **é seedada**: `seed_portal.py` popula as 13 linhas padrão (`FUNCOES_TELEFONIA_SEED`, `update_or_create` por `comando`) porque é documentação genérica de central telefônica, não dado específico da empresa — reexecutar `seed_portal` é seguro/idempotente, não duplica nem apaga linhas editadas manualmente (só atualiza `funcao`/`resumo` de um `comando` que já exista).
|
|
|
|
Nenhuma das duas subtelas tem endpoint de reorder — só criar/editar/excluir, mesmo escopo pedido.
|
|
|
|
## Modal de consulta rápida ("Ramais")
|
|
|
|
O botão "Ramais" do topbar (`#ramais-btn`, presente em `portal.html`/`links-ferramentas.html`/`calendario-individual.html` — as únicas 3 páginas que têm esse atalho; texto era "Acessar Ramais", encurtado depois) **não navega** para `ramais.html`; abre um modal somente-leitura (`ramais-lookup.js`/`ramais-lookup.css`) com a mesma listagem mesclada de `GET /api/ramais/`, inspirado numa tela do portal antigo (estilo DataTables: "Mostrar N registros", busca, colunas ordenáveis, paginação). Diferenças pro comportamento antigo do botão:
|
|
|
|
- Gate de acesso: some (`hidden`) se `permissoes_efetivas.ramais.apps["ramais-visualizar"]` for falso — mesmo padrão de qualquer UI gated por permissão no app.
|
|
- Busca é **uma só caixa** (não uma por coluna) que filtra por nome, departamento ou ramal ao mesmo tempo — mais simples que a paginação em duas caixas da própria `ramais.html`.
|
|
- **Botões de filtro por departamento** (`.ram-lookup-depto-filters`, acima da tabela): "Todos" + um botão por `Departamento` cadastrado, buscados de `GET /api/departamentos-resumo/` na primeira abertura (endpoint dedicado, `IsAuthenticated` + checagem manual de `permissao_app("ramais", "ramais-visualizar")` — não reaproveita `/api/departamentos/`, que exige `gerencia_permissoes` e bloquearia a maioria dos usuários que só têm acesso ao próprio Ramais). Clicar num botão filtra a listagem pra quem tem aquele departamento entre os seus (`departamento_exibicao.split(",")`, comparação exata após `trim` — não substring, pra não casar um departamento que seja prefixo de outro) e combina com a busca por texto (as duas condições precisam bater). Como os botões são gerados a partir da lista de departamentos vinda da API a cada abertura do modal, cadastrar um departamento novo em Usuários já basta pra ele aparecer aqui — não precisa mexer no frontend.
|
|
- Ordenação por coluna (clicar no cabeçalho alterna asc/desc) e paginação (`10`/`25`/`50`/`100` por página) são só client-side, sobre o array já carregado — sem endpoint novo, sem parâmetro de query; os `/api/ramais/`/`/api/departamentos-resumo/` são buscados uma única vez por abertura de página (cacheados em memória enquanto a página não recarrega) e refiltrados/reordenados em JS a cada tecla/clique.
|
|
- Botão "Ir para Controle de Ramais" no rodapé é o link de verdade pra `ramais.html` (tela completa, com edição) — o modal em si não tem nenhum controle de escrita, é só consulta.
|
|
|
|
## CSS
|
|
|
|
`ramais.css` (`.ram-*`) — a tabela em si reaproveita `.pa-table*`/`.pa-row-actions` de `perfis-acesso.css` (mesmo padrão que `usuarios.html` já usa sem CSS próprio). O modal de consulta rápida (`#ramais-btn`) tem CSS próprio em `ramais-lookup.css` (`.ram-lookup-*`), autocontido (não reaproveita `.pa-table` porque as 3 páginas que o usam não carregam `perfis-acesso.css`).
|
|
|
|
## API
|
|
|
|
| Endpoint | Método | Uso |
|
|
|---|---|---|
|
|
| `/api/ramais/` | GET | diretório **mesclado**: todo usuário ativo (linha montada do cadastro, sem precisar de nenhum registro extra) + linhas avulsas de `Ramal`; leitura exige `apps.visualizar` |
|
|
| `/api/ramais/`, `/api/ramais/{id}/` | POST/PATCH/DELETE | CRUD só das linhas avulsas (`Ramal`, sem `Usuario` por trás); escrita exige `apps.editar` |
|
|
| `/api/ramais/usuarios/` | GET | lista enxuta (`id`/`nome`) de usuários ativos pra alimentar o `<select>` "Lista de Usuários" do modal de Criar Ausência — não é `/api/usuarios/` de propósito |
|
|
| `/api/ramais/usuarios/{usuario_id}/` | PATCH | `{numero}` — grava direto em `Usuario.ramal`; é como a tela edita o ramal de um colaborador de verdade (exige `apps.editar`) |
|
|
| `/api/ramais-ausencias/`, `/api/ramais-ausencias/{id}/` | GET/POST/PATCH/DELETE | períodos de ausência; "ausente agora" nunca é lido daqui direto pelo frontend, vem calculado em `usuario_ausente`/`usuario_ausencia_ativa_id` na listagem de `/api/ramais/`; `PATCH` com `{"encerrada_manualmente": true}` encerra antes do previsto |
|
|
| `/api/telefones-externos/`, `/api/telefones-externos/{id}/` | GET/POST/PATCH/DELETE | subtela "Telefones Externos" de `ramais.html`; mesma permissão `PermissaoApp("ramais", ...)` do diretório de Ramais |
|
|
| `/api/funcoes-telefonia/`, `/api/funcoes-telefonia/{id}/` | GET/POST/PATCH/DELETE | subtela "Funções de Telefonia" de `ramais.html`; idem, mesma permissão de `ramais`; as 13 linhas padrão vêm de `seed_portal` |
|
|
|
|
> Estes endpoints moravam na tabela de API do `CLAUDE.md` da raiz e foram trazidos para cá: endpoint de aplicação mora na doc da aplicação. A raiz mantém só os transversais (auth, `/api/me/`, catálogo, perfis, usuários, favoritos, widgets, compromissos, notificações).
|