17 KiB
Ramais
Movido do
CLAUDE.mdda raiz em 2026-08-26 para reduzir conflito de edição entre aplicações. VerCLAUDE.mdna 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 emportal_api/models.py/views.py/serializers.pyjunto 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:
- Todo
Usuarioativo — a linha é montada direto do cadastro (nome,Usuario.departamentosjuntados por vírgula,Usuario.ramal); se o colaborador ainda não tem ramal preenchido,numero_exibicaovem vazio e o frontend mostra um badge "Pendente" em vez de reclamar de campo faltando. - As linhas avulsas de
Ramal(semUsuariopor 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 emUsuario.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/numerolivres — sónomeé obrigatório, o resto pode ficar pendente igual a um colaborador de verdade). Editar/excluir uma linha avulsa usaPATCH/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/nomede 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 agerencia_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 queRamalViewSet.list()monta a resposta. Clicar no badge "(AUSENTE)" de uma linha (data-ram-ver-ausencia, qualquer um comapps.visualizarpode abrir) fazGET /api/ramais-ausencias/{id}/e abre o modal "Visualizar Ausência" — campos desabilitados (<input type="date"/"time">mostra a data/hora formatada mesmodisabled, sem precisar formatar manualmente), com "Editar Ausência"/"Deletar Ausência" visíveis só pra quem temapps.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 viaencerrada_manualmente; substituído por editar/excluir de verdade, mais simples e é o que foi pedido). O campoencerrada_manualmentecontinua 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) comtimezone.localdate(), feita no mesmolist()— 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/--goldcrus 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--aniversariotd, aplicado via classe no<tr>emramais.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). Osrcdo iframe só é setado na abertura do modal e volta praabout:blankao 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()emlist()), semordem/drag-and-drop. - Usuário inativo (
is_active=False) não aparece mais no diretório (olist()filtraUsuario.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) sepermissoes_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 porDepartamentocadastrado, buscados deGET /api/departamentos-resumo/na primeira abertura (endpoint dedicado,IsAuthenticated+ checagem manual depermissao_app("ramais", "ramais-visualizar")— não reaproveita/api/departamentos/, que exigegerencia_permissoese 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óstrim— 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/100por 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.mdda 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).