portal_publico/docs/ramais/ramais.md

17 KiB

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):

"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).