96 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Contexto do projeto
Portal interno da De Paula Contadores ("Portal De Paula"). Portal/ é o próprio projeto Django — Python 3.13 + Django 6.0 + Django REST Framework + PostgreSQL 14 — organizado no padrão convencional de um projeto Django (manage.py na raiz, app portal_api/, templates/, static/), servindo tanto a API (/api/...) quanto o frontend HTML/CSS/JS (mesma origem — ver "Arquitetura" abaixo).
Até uma rodada anterior, todo o estado (sessão, usuários, perfis de acesso, favoritos, widgets, compromissos) vivia no localStorage do navegador — não havia backend. Isso mudou: o usuário decidiu a stack real e pediu a migração completa desses dados para o banco. Ver plano.md para o histórico de decisões rodada a rodada; consultar antes de mudar algo que pareça uma limitação (ex.: ausência de teste automatizado, remoção do calendário interno antigo) sem confirmar se foi decisão deliberada.
O que continua só no localStorage: apenas a preferência de tema (claro/escuro e cor do tema) — é preferência de navegador, não dado de negócio, e ficou fora do escopo da migração por decisão explícita do usuário.
Documentação dividida por aplicação
Este arquivo cobre o que é transversal ao Portal (arquitetura, modelo de permissões, API, CSS, animações). A partir de 2026-08-26, a documentação detalhada de cada aplicação foi movida pra fora daqui, pra reduzir conflito de edição quando mais de uma pessoa mexe em aplicações diferentes ao mesmo tempo. Ver prd.md pra visão de produto (o quê/pra quem), README.md na raiz pro mapa de todas as aplicações, e plano.md pro histórico de decisões estruturais/transversais (o histórico específico de cada aplicação vive no CHANGELOG.md dela, ver abaixo).
Cada aplicação tem uma pasta própria com (a) o detalhamento técnico do estado atual, (b) um README.md (resumo enxuto — o quê/pra quem, sem o detalhe técnico) e (c) um CHANGELOG.md (histórico rodada a rodada, extraído de plano.md, mesma numeração de rodada usada lá):
Aplicações com pacote Python próprio (CLAUDE.md carregado automaticamente pelo Claude Code ao trabalhar dentro da pasta):
| Aplicação | Onde |
|---|---|
| Importação de Plano de Saúde | portal_api/planos_saude/ — CLAUDE.md (técnico), README.md, CHANGELOG.md |
| Indicador de Desempenho | portal_api/indicadores/ — CLAUDE.md (técnico), README.md, CHANGELOG.md |
| Simulação de Custo de Contratação | portal_api/custo_contratacao/ — CLAUDE.md (técnico), README.md, CHANGELOG.md |
| Não Conformidades | portal_api/nao_conformidades/ — CLAUDE.md (técnico), README.md, CHANGELOG.md |
| Dashboard Contábil | portal_api/dashboard_contabil/ — CLAUDE.md (técnico), README.md, CHANGELOG.md |
Aplicações sem pacote Python dedicado (código ainda em portal_api/models.py/views.py/serializers.py — os arquivos em docs/<app>/ não são carregados automaticamente, ler manualmente):
| Aplicação | Onde |
|---|---|
| Ramais (diretório, Telefones Externos, Funções de Telefonia, modal de consulta rápida) | docs/ramais/ — ramais.md (técnico), README.md, CHANGELOG.md |
| Links & Ferramentas / Acessos Gerais | docs/links-ferramentas-acessos-gerais/ — links-ferramentas-acessos-gerais.md (técnico), README.md, CHANGELOG.md |
Calendário Individual e Widgets (incl. Eventos Corporativos, feriados, widgets de portal.html) |
docs/calendario-individual/ — calendario-individual.md (técnico), README.md, CHANGELOG.md |
Favoritos (grade de portal.html) |
docs/favoritos/ — favoritos.md (técnico), README.md, CHANGELOG.md |
| Perfis de Acesso / Usuários (telas administrativas, Liderança, inativação) | docs/perfis-usuarios/ — perfis-usuarios.md (técnico), README.md, CHANGELOG.md |
| Solicitações | docs/solicitacoes/ — solicitacoes.md (técnico), README.md, CHANGELOG.md |
Como rodar / testar localmente
Backend (obrigatório para qualquer teste agora — o frontend não funciona mais sozinho via file:///http.server)
cd Portal
python -m venv .venv
.venv\Scripts\activate # Windows
pip install -r requirements.txt
Configurar as variáveis de ambiente do Postgres 14 antes de migrar — config/settings.py chama load_dotenv(BASE_DIR / ".env") e lê DB_NAME, DB_USER, DB_PASSWORD, DB_HOST, DB_PORT (o arquivo .env já existe na raiz de Portal/):
python manage.py makemigrations portal_api
python manage.py migrate
python manage.py runserver
python manage.py seed_portal é só para ambiente novo/vazio (primeira criação do banco) — não rodar mais neste ambiente. O Portal já está em produção: os 8 perfis de acesso e todas as contas de usuário (incluindo gabriel/bruno) já existem de verdade, com senhas e permissões mantidas pelos próprios usuários direto pela tela (Perfis de Acesso/Usuários) — não mais pelo seed. seed_portal resincroniza incondicionalmente permissoes/ativo/gerencia_permissoes dos 8 perfis de código fixo a partir de catalogo.py a cada execução (ver "Cuidado com seed_portal.py" mais abaixo) — rodar isso contra o banco já em uso reverteria qualquer permissão que um admin tenha customizado manualmente para um desses 8 perfis, sem nenhum aviso. Ao adicionar uma aplicação nova ao catálogo (novo app_key), a chave nova ainda entra automaticamente pros perfis certos na próxima vez que seed_portal rodar — mas evitar rodar só por causa disso; se for mesmo necessário, avisar o usuário antes e confirmar, e idealmente conferir os 8 perfis em Perfis de Acesso depois pra garantir que nenhuma customização foi perdida.
Não há suíte de testes, lint ou build configurados neste projeto.
Ambiente de desenvolvimento assistido
O .venv do projeto já tem Python 3.13 + Django 6.0 + DRF + psycopg + python-dotenv + Pillow + nh3 + reportlab + openpyxl + holidays + docling instalados, e o Postgres acessível via .env é o banco de produção (não uma cópia de desenvolvimento) — dá pra rodar makemigrations/migrate/runserver normalmente por aqui usando .venv\Scripts\python.exe manage.py ... (ou ativando o venv primeiro), mas nunca seed_portal (ver "Como rodar / testar localmente" acima — recria/ressincroniza perfis já em uso de verdade). Isso deixou de ser uma limitação a partir da rodada em que o ambiente ganhou essas ferramentas (ver plano.md) — não assumir mais que só é possível revisar o backend estaticamente.
Arquitetura
Estrutura de pastas (padrão Django)
Portal/
├── manage.py
├── requirements.txt
├── .env
├── config/ # settings.py, urls.py, wsgi.py, asgi.py — pacote de configuração do projeto
├── portal_api/ # único app Django (models, serializers, views, admin, migrations, seed)
├── templates/ # as 8 páginas HTML (TEMPLATES[0]["DIRS"] em settings.py aponta pra cá)
├── static/ # css/, js/, img/ — STATICFILES_DIRS em settings.py aponta pra cá
├── media/ # upload de usuário (hoje só ícones de LinkFerramenta) — MEDIA_ROOT em settings.py
├── CLAUDE.md
└── plano.md
Logos em static/img/
Duas identidades visuais coexistem de propósito hoje: o logo cursivo "D De Paula Contadores" (usado só nos documentos/PDFs que a aplicação gera, ver abaixo) e a marca nova "P.I.D." (.svg, ver pid-marca-leiame.md em C:\Users\Depaula\Documents\Projetos\LOGOS\pid-marca), adotada no favicon, no login e na sidebar do Portal. Essa separação é deliberada, não uma migração incompleta: um documento gerado pela aplicação (contrato, procuração, recibo do Indicador de Desempenho, PDF da Simulação de Custo de Contratação) é emitido como se o próprio escritório o tivesse gerado — carrega a identidade do escritório perante o cliente, não a identidade do Portal como ferramenta interna. A marca "P.I.D." é só pra UI do Portal em si. Não migrar o logo de um gerador de documento pra "P.I.D." (nem vice-versa numa tela do Portal) sem confirmar de novo com o usuário.
Logo cursivo "D De Paula Contadores" (D em degradê dourado/marrom + texto, PNG com fundo transparente) — não aparece em nenhum template HTML hoje, só nos PDFs gerados pela aplicação:
logo.png— original, texto preto. Serve como fonte pra gerar as outras variantes e é usada diretamente no recibo do Indicador de Desempenho (indicadores/recibo.py,LOGO_PATH, redimensionada/recomprimida em memória pra impressão — verportal_api/indicadores/CLAUDE.md).logo-branco.png— usada no cabeçalho do PDF de Simulação de Custo de Contratação (custo_contratacao/pdf.py, banner marrom escuro, verportal_api/custo_contratacao/CLAUDE.md) — até uma rodada anterior também era usada nosidebar__branddos 10 shells, migrada pra marca "P.I.D." (ver abaixo; a UI do Portal e os documentos gerados usam fontes de logo independentes agora). Mesmo D colorido delogo.png, mas com o texto recolorido pra branco; gerada programaticamente a partir delogo.png(script Python com Pillow: qualquer pixel opaco quase-neutro/escuro —max(r,g,b) < 70espread(r,g,b) < 12— virou branco; o D nunca entra nesse filtro porque mesmo na sombra mais escura do degradê ele mantém um matiz quente nitidamente não-neutro). Se o logo oficial mudar, regerarlogo-branco.pnga partir do novologo.pngcom o mesmo filtro, não editar à mão.logo-mono.png— versão totalmente monocromática (D e texto em branco/cinza claro). Não usada em nenhum consumidor hoje; existe como variante alternativa (útil se algum dia precisar de um logo "chapado" sem o dourado do D).
Marca nova "P.I.D.":
pid-icone.svg— ícone quadrado, variante pra fundo claro (corpo roxo escuro#4B2E75); chegou a ser usada no login quandodata-theme="light", mas o usuário pediu pra usar sempre a mesma logo nos dois temas — não tem mais nenhum consumidor hoje (mesma situação depid-favicon.svg/pid-logo-horizontal.svgabaixo).pid-icone-escuro.svg— ícone quadrado, variante pra fundo escuro (corpo roxo mais claro#7B5BA8, pra manter contraste); usada (a) no favicon (<link rel="icon" type="image/svg+xml">no<head>das 11 páginas) e (b) no login, sempre, nos dois temas — o card do login já é congelado escuro nos dois temas (ver "Card de login" abaixo), e agora o ícone também, pedido explícito do usuário ("deixe no tema claro a mesma logo usada no tema escuro"; antes alternava compid-icone.svgconformedata-theme, mecanismo removido — ver "Ícone do login é clicável" abaixo). O ícone da sidebar (expandida e colapsada, 10 shells) e o do card de login usam o mesmo desenho/cores desse arquivo, mas como markup<svg>inline copiado direto no HTML, não uma referência a este arquivo — ver "Ícone do login/sidebar são clicáveis" abaixo pro motivo (precisa expor os olhos pro CSS/JS animar o piscar ao clicar).pid-logo-horizontal-escuro.svg— assinatura horizontal (ícone + "P.I.D." + "PORTAL INTERNO DA DE PAULA" em texto, viewBox300×80, texto claro#F2EDE3/dourado#C6A24A— variante pra fundo escuro). Mesmo caso do ícone acima: a sidebar expandida (10 shells) usa o mesmo desenho como<svg>inline, não uma referência a este arquivo.pid-favicon.svg(versão simplificada sem o sorriso, pro leiame recomendar pra 16–24px — não usada, o favicon usapid-icone-escuro.svgmesmo),pid-logo-horizontal.svg(variante fundo claro da assinatura) epid-icone.svg(acima) não têm nenhum consumidor no Portal hoje.
sidebar__brand com duas marcas sobrepostas (crossfade), não uma redimensionada (layout.css): a sidebar expandida mostra a assinatura com texto (tem texto, ilegível se só encolhida) e a colapsada mostra só o símbolo — são dois <svg class="sidebar__logo sidebar__logo--full">/<svg class="sidebar__logo sidebar__logo--icon"> sempre presentes no DOM, sobrepostos via position:absolute dentro de .sidebar__brand (position:relative; height:92px, alto o bastante pra caber as duas sem depender da altura natural de nenhuma, já que filhos absolutos não contribuem pra altura do pai). .sidebar__logo--full é width:250px; max-width:96% (quase toda a largura útil da sidebar, 264px menos o padding horizontal de .sidebar) — aumentado em duas rodadas a partir do tamanho inicial (176px/70% → 220px/92% → 250px/96%) porque o subtítulo "PORTAL INTERNO DA DE PAULA" (fonte pequena dentro do SVG, viewBox 300×80) ficava ilegível menor; height:92px acompanhou cada aumento pra sobrar espaço vertical (proporção do SVG é 300:80, então a altura renderizada escala junto com a largura). A transição entre elas é opacity+transform:scale() (var(--transition-base), mesma duração das outras animações do sidebar) — pedido explícito do usuário pra não ser uma troca brusca; .app-shell.is-collapsed (toggle desktop) e o breakpoint mobile (@media (max-width:1024px), onde o colapsado é o estado default e .is-expanded-mobile o inverte, mesmo padrão já usado pelos demais elementos do menu nesse breakpoint) alternam qual das duas fica com opacity:1.
Ícone do login e da sidebar são clicáveis, com os olhos piscando (pedido explícito do usuário, em duas rodadas — primeiro a sidebar, depois o ícone do login): tanto .sidebar__brand (10 shells) quanto #login-logo-btn (index.html) tiveram o ícone convertido de <img src="...svg"> pra <svg> inline, com markup idêntico ao de pid-icone-escuro.svg copiado direto no HTML — um <img> não expõe seu conteúdo interno pro CSS/JS da página (é uma imagem opaca), então não dava pra animar só os olhos sem inlinear. Dentro de cada SVG, os dois olhos (círculo creme + glint escuro) ficam num <g class="pid-icon-eye"> próprio, sem nenhum transform no XML (a posição já vem dos cx/cy dos círculos) — importante porque um transform de CSS aplicado num elemento que já tem um transform de atributo substitui o atributo inteiro (perderia a posição); mantendo os dois olhos "limpos" desse jeito, a única transformação deles é a que a animação de piscar aplica. .pid-icon-eye/.is-blinking/@keyframes pidIconBlink moram em base.css (transform-box:fill-box; transform-origin:center faz o scaleY() girar em torno do próprio olho, não da origem do SVG; scaleY(1)→0.05→1, 200ms, os dois olhos piscam juntos) — em base.css, não em layout.css, porque é carregado por todas as páginas, inclusive index.html (que não carrega layout.css).
- Sidebar (
static/js/sidebar-brand.js, incluído logo depois deapi.jsnos 10 shells):.sidebar__branddeixou de ser<div>e virou<a href="portal.html" id="sidebar-brand-link" aria-label="Ir para a tela Principal">— a mudança de tag não afeta o CSS existente (todo seletor é por classe), então o crossfade descrito acima continua igual. O script escuta o clique: ignora cliques modificados (ctrl/cmd/shift/botão do meio — deixa abrir em nova aba normalmente), senão fazpreventDefault(), adiciona.is-blinkingem todo.pid-icon-eyedentro do link (pega os olhos das duas marcas — a visível e a escondida pelo crossfade, inofensivo já que a escondida temopacity:0) e só navega praportal.htmldepois de 260ms, tempo suficiente pra piscada terminar de tocar antes da página trocar — mesmo espírito de "deixar a transição ser vista antes de navegar" já usado na animação de intro do login. - Login (
static/js/login-logo-blink.js, novo):#login-logo-btné um<button type="button">(não um link — não há pra onde navegar a partir do próprio login), sempreventDefault/delay nenhum, só dispara a piscada ao clicar; é puramente decorativo, sem efeito colateral. Também aqui foi a oportunidade de simplificar um mecanismo que existia só por causa do<img>: a logo do login alternava entrepid-icone.svg(claro)/pid-icone-escuro.svg(escuro) conformedata-theme, viapidSyncLoginLogo()emtheme.js— removida junto com essa mudança, a pedido do usuário ("deixe no tema claro a mesma logo usada no tema escuro"), já que o card do login já era congelado escuro nos dois temas mesmo antes disso (ver "Card de login" abaixo) — a lógica de alternância nunca fazia muito sentido nesse contexto. Agora#login-logo-btnsempre usa as cores/desenho depid-icone-escuro.svg, sem nenhuma checagem de tema.
Backend serve o frontend (mesma origem)
config/urls.py registra path("api/", include("portal_api.urls")) e, para cada página HTML do frontend (index.html, portal.html, perfis-acesso.html, usuarios.html, calendario-individual.html, links-ferramentas.html, acessos-gerais.html, ramais.html, importacao-plano-saude.html, custo-contratacao.html, indicador-desempenho.html, nao-conformidades.html), uma rota TemplateView que resolve o arquivo em templates/. Os estáticos (static/css, static/js, static/img) são servidos por django.contrib.staticfiles automaticamente em DEBUG (via STATICFILES_DIRS) — não há mais nenhum re_path/static_serve manual em urls.py. Cada template usa {% load static %} + {% static 'css/tokens.css' %} (nunca um caminho hardcoded tipo assets/css/..., que não existe mais). Essa escolha (Django servindo o próprio frontend) existe para evitar CORS/cookie cross-origin: autenticação é por sessão/cookie do Django, então frontend e API precisam estar na mesma origem.
Em produção, rodar python manage.py collectstatic (junta tudo em STATIC_ROOT = BASE_DIR / "staticfiles") e servir esse diretório via whitenoise/nginx — django.contrib.staticfiles só serve automaticamente quando DEBUG=True. Uploads de usuário (ícones de LinkFerramenta) são um mecanismo separado: MEDIA_URL/MEDIA_ROOT em settings.py, servidos por config/urls.py via static() só quando DEBUG=True (em produção, servir media/ também por whitenoise/nginx, igual ao STATIC_ROOT).
Apps Django
Um único app, portal_api/:
| Arquivo | Conteúdo |
|---|---|
models.py |
Usuario (AbstractUser + nome, M2M perfis, M2M departamentos (pra Departamento, ver abaixo), campos cadastrais opcionais codigo_folha/codigo_questor/codigo_tareffa/codigo_contabit/ramal (CharField, blank=True) e data_aniversario (DateField, null=True, blank=True), lideranca (BooleanField, é gerente/coordenador) e M2M liderados (self-referential, symmetrical=False, related_name="lideres" — ver docs/perfis-usuarios/perfis-usuarios.md) — email já vem de AbstractUser, não precisou de campo novo; método permissao_app(module_key, app_key) — união genérica de um flag de apps entre os perfis vinculados; método eh_perfil_inovacao() — checagem de nome fixo pro perfil "Inovação", ver "Ajuda de aplicação" abaixo), AjudaAplicacao (texto de "Mais informações" de uma aplicação, chave natural app_key), Departamento (só nome, unique=True — cadastro inline pela própria tela de Usuários, sem tela de administração dedicada como PerfilAcesso), PerfilAcesso (permissoes em JSONField, mesmo formato aninhado do frontend; mais o booleano dedicado gerencia_permissoes), CompromissoAgenda, Favorito, WidgetUsuario, NotificacaoDispensada, LinkFerramenta (icone é ImageField, requer Pillow), LinkFerramentaFavorito (favorito por usuário de um cartão de Links & Ferramentas — não confundir com Favorito), AcessoGeralSecao/AcessoGeral (cadastro de logins/acessos compartilhados da aplicação "Acessos Gerais", ver docs/links-ferramentas-acessos-gerais/links-ferramentas-acessos-gerais.md), Ramal (linha avulsa da tela de Ramais, sem Usuario por trás — colaboradores de verdade aparecem automaticamente na listagem, sem precisar de uma linha aqui; ver docs/ramais/ramais.md), RamalAusencia (período de ausência de um colaborador, com esta_ativa() calculando "ausente agora" em vez de armazenar), TelefoneExterno (subtela "Telefones Externos" de Ramais, sem Usuario por trás), FuncaoTelefonia (subtela "Funções de Telefonia" de Ramais, Meta.ordering por comando reproduz a ordem esperada sem campo de ordem manual), ImportacaoPlanoSaude/ImportacaoPlanoSaudeLinha/ImportacaoPlanoSaudeAuditoria/ImportacaoPlanoSaudeAlteracao/VinculoNomeOperadora (ferramenta "Importação de Plano de Saúde" em Utilitários, ver portal_api/planos_saude/CLAUDE.md, inclusive "Vínculos de nome salvos (DE/PARA)"). |
catalogo.py |
Fonte única da verdade do catálogo de módulos/aplicações do menu (MODULES, MODULE_APPS) — exposto só leitura via GET /api/catalogo/. Ao adicionar uma seção/aplicação nova ao menu, editar aqui, não em static/js/profiles.js (que só cacheia o payload recebido). |
serializers.py |
PerfilAcessoSerializer, DepartamentoSerializer, UsuarioResumoSerializer (id/nome/departamentos, usado nos dois lados de liderados e por /api/usuarios-resumo/), UsuarioSerializer (escrita, aceita senha+perfis+departamentos+liderados)/UsuarioListSerializer (leitura, perfis/departamentos/liderados aninhados), CompromissoAgendaSerializer (sou_dono, dono_nome, dono_username), FavoritoSerializer, WidgetUsuarioSerializer, NotificacaoDispensadaSerializer, LinkFerramentaSerializer, LinkFerramentaFavoritoSerializer, AcessoGeralSecaoSerializer, AcessoGeralSerializer, RamalSerializer (só das linhas avulsas — ver seção "Ramais"), RamalAusenciaSerializer, TelefoneExternoSerializer, FuncaoTelefoniaSerializer, ImportacaoPlanoSaudeCreateSerializer/ImportacaoPlanoSaudeListSerializer/ImportacaoPlanoSaudeDetailSerializer/ImportacaoPlanoSaudeLinhaSerializer/ImportacaoPlanoSaudeAuditoriaSerializer (ver seção "Importação de Plano de Saúde"). |
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 docs/links-ferramentas-acessos-gerais/links-ferramentas-acessos-gerais.md/docs/ramais/ramais.md). |
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; 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 portal_api/indicadores/CLAUDE.md. |
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 portal_api/planos_saude/CLAUDE.md. |
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 portal_api/custo_contratacao/CLAUDE.md. |
indicadores/ |
Pacote Python puro (sem ORM) da ferramenta "Indicador de Desempenho" (Geradoc) — tipos.py (deriva o tipo de colaborador por empresa via Tareffa), leiaute.py (leitura das planilhas Tareffa/Honorários via openpyxl), pipeline.py (orquestração, processa_apuracao), entregas.py (cálculo dos 3 critérios automáticos), calculo.py (composição dos percentuais Individual/Grupo/Departamento e valores em R$), recibo.py (PDF do recibo por colaborador, via reportlab). Ver portal_api/indicadores/CLAUDE.md. |
API (sessão + CSRF, não token)
| Endpoint | Método | Uso |
|---|---|---|
/api/auth/csrf/ |
GET | garante o cookie csrftoken |
/api/auth/login/ |
POST | {username, password} → cria sessão |
/api/auth/logout/ |
POST | encerra sessão |
/api/me/ |
GET | usuário logado + perfis + departamentos (os próprios, pra alimentar o seletor de "Meu departamento" do Calendário Individual) + gerencia_permissoes + eh_perfil_inovacao (perfil "Inovação" vinculado, ver "Ajuda de aplicação" abaixo) + permissoes_efetivas (união já calculada no servidor) |
/api/me/senha/ |
POST | {senha_atual, nova_senha} |
/api/me/liderados/ |
PATCH | {liderados: [id, ...]} — só se me.lideranca; auto-gerenciamento de liderados (ver docs/perfis-usuarios/perfis-usuarios.md) |
/api/catalogo/ |
GET | módulos/aplicações/subgrupos do menu |
/api/feriados/?ano=AAAA |
GET | feriados nacionais + estaduais (PR) do ano pedido (default: ano atual), via lib holidays — ver docs/calendario-individual/calendario-individual.md |
/api/usuarios-resumo/ |
GET | lista enxuta (id/nome) de usuários ativos — alimenta o seletor de liderados, sem exigir gerencia_permissoes (mesmo padrão de /api/ramais/usuarios/) |
/api/departamentos-resumo/ |
GET | lista enxuta (id/nome) de departamentos — alimenta os botões de filtro do modal de consulta rápida de Ramais, exige só ramais-visualizar (não gerencia_permissoes como /api/departamentos/) |
/api/ajuda-aplicacoes/<app_key>/ |
GET/PATCH | texto de "Mais informações" de uma aplicação (ver seção própria abaixo); GET livre a qualquer autenticado, PATCH exige eh_perfil_inovacao (perfil "Inovação", checagem de nome fixo, não uma flag em Perfis de Acesso) |
/api/perfis/, /api/perfis/{codigo}/ |
GET/POST/PUT/DELETE | CRUD de perfil — só quem tem gerencia_permissoes |
/api/departamentos/, /api/departamentos/{id}/ |
GET/POST/PUT/DELETE | CRUD de departamento — só quem tem gerencia_permissoes; usado pela tela de Usuários pra listar o checklist e cadastrar um novo departamento inline (sem tela própria) |
/api/usuarios/, /api/usuarios/{id}/ |
GET/POST/PATCH/DELETE | CRUD de conta — só quem tem gerencia_permissoes; departamentos é M2M igual perfis (lista de ids na escrita, objetos aninhados na leitura); is_active é gravável via PATCH (inativar/reativar, ver docs/perfis-usuarios/perfis-usuarios.md) |
/api/compromissos/, /api/compromissos/{id}/ |
GET/POST/PATCH/DELETE | GET já retorna só o que o usuário logado pode ver (próprios + visibilidade="todos" + visibilidade="departamento" com departamento em comum); campo notificar_em (calculado, ver docs/calendario-individual/calendario-individual.md) indica quando o lembrete passa a valer; criar/editar com visibilidade em departamento/todos (inclusive todo eh_evento=True, que força visibilidade="todos") exige apps["calendario-individual-criar-evento"] (ver "Eventos Corporativos" abaixo) |
/api/categorias-evento/, /api/categorias-evento/{id}/ |
GET/POST/PATCH/DELETE | cadastro de categorias de evento (nome+cor) usado pelo Calendário Individual; GET livre a qualquer autenticado, escrita exige apps["calendario-individual-criar-evento"] — ver docs/calendario-individual/calendario-individual.md |
/api/favoritos/, /api/favoritos/{app_id}/ |
GET/POST/PATCH/DELETE | chave natural é app_id, não um id numérico; ordem é gravável via PATCH (drag-and-drop na grade de favoritos, ver docs/favoritos/favoritos.md) |
/api/widgets/, /api/widgets/{tipo}/ |
GET/POST/PATCH/DELETE | chave natural é tipo; ordem (reordenar por drag-and-drop) e largura/altura em px (redimensionamento) também são graváveis via PATCH — ver docs/calendario-individual/calendario-individual.md |
/api/notificacoes-dispensadas/, /api/notificacoes-dispensadas/{notif_id}/ |
GET/POST/DELETE | chave natural é notif_id (ex.: "tool-widgets", "event-42"); notifications.js usa GET pra filtrar o que já foi dispensado e POST a cada X/"Limpar tudo" |
/api/links-ferramentas/, /api/links-ferramentas/{id}/ |
GET/POST/PATCH/DELETE | lista compartilhada (não por usuário); leitura exige apps["links-ferramentas-visualizar"] e escrita exige apps["links-ferramentas-editar"] em permissoes["links-ferramentas"] (PermissaoApp, gate por método em get_permissions() — ver "Modelo de permissões" abaixo); POST é multipart/form-data (aceita upload de icone); ordem sempre é atribuída pelo servidor na criação (ignora o que vier no payload), reordenar é PATCH trocando o ordem de dois itens |
/api/links-ferramentas-favoritos/, /api/links-ferramentas-favoritos/{link_id}/ |
GET/POST/DELETE | favorito por usuário de um cartão (chave natural é link_id, o id do LinkFerramenta — mesmo padrão de app_id/notif_id); exige só apps["links-ferramentas-visualizar"] (favoritar não precisa de editar); só afeta a ordem de exibição em Links & Ferramentas e o widget "Links Favoritos", nunca o ordem compartilhado (ver docs/links-ferramentas-acessos-gerais/links-ferramentas-acessos-gerais.md) |
/api/acessos-gerais-secoes/, /api/acessos-gerais-secoes/{id}/ |
GET/POST/PATCH/DELETE | seções do cadastro "Acessos Gerais" (aplicação dentro da seção Links & Ferramentas); leitura exige apps["acessos-gerais-visualizar"], escrita exige apps["acessos-gerais-editar"]; excluir uma seção também exclui (CASCADE) os acessos dela; GET só lista seções sem perfis_restritos ou com interseção com os perfis do usuário (ver docs/links-ferramentas-acessos-gerais/links-ferramentas-acessos-gerais.md) |
/api/acessos-gerais/, /api/acessos-gerais/{id}/ |
GET/POST/PATCH/DELETE | linhas (acessos/logins) dentro de uma seção; mesma permissão de acessos-gerais-secoes; ordem é escopada por secao (servidor calcula max(ordem) só entre as linhas da mesma seção) — ver docs/links-ferramentas-acessos-gerais/links-ferramentas-acessos-gerais.md |
/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 — ver docs/ramais/ramais.md |
/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 (ver docs/ramais/ramais.md) |
/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 |
/api/importacoes-plano-saude/, /api/importacoes-plano-saude/{id}/ |
GET/POST | histórico + criação (ver portal_api/planos_saude/CLAUDE.md); PermissaoApp("utilitarios", "importacao-plano-saude") (toggle único) pra todos os métodos; POST é multipart/form-data (planilha padrão + arquivo da operadora) e roda o pipeline de forma síncrona antes de responder |
/api/importacoes-plano-saude/operadoras/ |
GET | [{key, label}] das operadoras registradas em planos_saude.pipeline.OPERADORAS — alimenta o <select> do formulário |
/api/importacoes-plano-saude/regras-empresa/ |
GET | [{key, label}] das regras especiais registradas em planos_saude.regras_empresa.REGRAS_EMPRESA — alimenta o modal "Selecionar regra" do checkbox "Regra empresa" (ver portal_api/planos_saude/CLAUDE.md) |
/api/importacoes-plano-saude/{id}/gerar/ |
POST | monta o CSV (ou ZIP, se mais de um tipo de lançamento) a partir das linhas já revisadas/editadas e devolve como download binário; marca a importação como concluida |
/api/importacoes-plano-saude-linhas/, /api/importacoes-plano-saude-linhas/{id}/ |
GET/POST/PATCH/DELETE | edição/inclusão/exclusão de uma linha da revisão (todos os campos, não só valores); mesma permissão da importação, sem conceito de "dono"; as três operações também gravam um ImportacaoPlanoSaudeAlteracao (ver portal_api/planos_saude/CLAUDE.md) |
/api/importacoes-plano-saude-alteracoes/{id}/reverter/ |
POST | desfaz uma alteração específica (edição/inclusão/exclusão de linha) registrada na aba "Alterações" da revisão — ver portal_api/planos_saude/CLAUDE.md |
/api/regras-custeio-plano-saude/, /api/regras-custeio-plano-saude/{id}/ |
GET/POST/PATCH/DELETE | banco de regras de custeio por empresa+operadora (codigo_empresa+operadora, únicos juntos+regra_empresa_chave+tipos_lancamento+custeio_por_tipo+observacoes; nome é sempre derivado, nunca aceito do cliente — ver portal_api/planos_saude/CLAUDE.md) — mesma permissão de toggle único da ferramenta; lista compartilhada, sem "dono"; cadastro/edição só pela tela "Cadastro de Regras", nunca em "Nova Importação" |
/api/simulacao-custo-contratacao/gerar/ |
POST | calcula (portal_api.custo_contratacao.calculo.calcula_custo_empregado) e devolve o PDF direto na resposta (application/pdf, sem persistir nada); PermissaoApp-like check manual via permissao_app("geradoc", "simulacao-custo-contratacao") — ver portal_api/custo_contratacao/CLAUDE.md |
/api/parametros-fiscais-custo-contratacao/ |
GET/PATCH | tabelas de INSS/IRRF + parâmetros da Lei 15.270/2025 usados pela simulação (ParametroFiscalCustoContratacao, singleton pk=1); mesma permissão da simulação, sem par visualizar/editar dedicado |
/api/indicadores-percentuais-tipo/ |
GET/POST/DELETE | histórico de percentuais individual/grupo/departamento por tipo de colaborador (IndicadorPercentualTipo) — nunca editado in-place, só criado com vigente_desde novo; mesma permissão de toggle único apps["indicador-desempenho"] em permissoes["geradoc"] |
/api/indicadores-criterios/, /api/indicadores-criterios/{id}/ |
GET/POST/PATCH/DELETE | CRUD do cadastro genérico de critérios (IndicadorCriterio) — nome/grupo/peso/período/papel/cálculo automático livres, editável pelo RH |
/api/indicadores-apuracoes/, /api/indicadores-apuracoes/{id}/ |
GET/POST/DELETE | apuração mensal (IndicadorApuracao); POST é multipart (2 planilhas) e roda indicadores.pipeline.processa_apuracao() de forma síncrona dentro de um transaction.atomic(), persistindo colaboradores/empresas/respostas já calculados; DELETE também apaga os 2 arquivos de MEDIA_ROOT |
/api/indicadores-apuracoes/{id}/gerar/ |
POST | gera um ZIP com um PDF de recibo por colaborador (indicadores.recibo.gera_pdf_recibo), a partir do que já está salvo (não reprocessa as planilhas); colaborador_ids opcional no corpo restringe a geração a só esses colaboradores (modal "Gerar Recibos" — um colaborador só, alguns específicos, por departamento ou todos); marca a apuração como concluida só quando a seleção cobre todos os colaboradores |
/api/indicadores-apuracoes/{id}/ajustar-grupo/, /recalcular-grupo/ |
POST | ajusta (ou reverte) o pct_grupo de todos os colaboradores de um mesmo gerente na apuração de uma vez — "cada gerente representa um grupo" (ver portal_api/indicadores/CLAUDE.md) |
/api/indicadores-apuracoes/{id}/ajustar-departamento/, /recalcular-departamento/ |
POST | idem, mas aplica a todos os colaboradores do departamento (id de um IndicadorDepartamento) informado no corpo ({departamento, pct_departamento}/{departamento}) — cada departamento tem sua própria meta de Departamento, ver portal_api/indicadores/CLAUDE.md |
/api/indicadores-departamentos/, /api/indicadores-departamentos/{id}/ |
GET/POST/PATCH/DELETE | cadastro de departamentos (IndicadorDepartamento, nome/ativo) — mesma permissão de toggle único do Indicador de Desempenho, ver portal_api/indicadores/CLAUDE.md |
/api/indicadores-departamentos-gerentes/, /api/indicadores-departamentos-gerentes/{id}/ |
GET/POST/PATCH/DELETE | relação gerente→departamento (IndicadorDepartamentoGerente, nome_gerente único) — mesma permissão, ver portal_api/indicadores/CLAUDE.md |
/api/indicadores-apuracoes/{id}/ajustar-honorario-empresa/ |
POST | {codigo_empresa, honorario} — preenche (ou corrige) o honorário de uma empresa com honorario_nao_encontrado=True ou honorario_ajustado_manualmente=True de uma vez pra todos os colaboradores desta apuração que a têm (mesmo código), recalculando cada um (ver portal_api/indicadores/CLAUDE.md) |
/api/indicadores-apuracoes-colaboradores/{id}/ |
GET/PATCH | ajuste manual do pct_individual de um colaborador (pct_individual_ajustado_manualmente=True); recalcula valor_total via indicadores.calculo.recalcula_colaborador |
/api/indicadores-apuracoes-colaboradores/{id}/recalcular/ |
POST | reverte pct_individual pro modo automático (limpa o ajuste manual) e recalcula |
/api/indicadores-apuracoes-colaboradores/{id}/marcar-validado/ |
POST | {validado} — checklist de revisão do RH, só grava o campo, sem recalcular nada (ver portal_api/indicadores/CLAUDE.md) |
/api/indicadores-apuracoes-empresas/{id}/trocar-responsavel/ |
POST | {colaborador_id} — reatribui essa linha (empresa+tipo) pra outro colaborador da mesma apuração, recalculando os dois (ver portal_api/indicadores/CLAUDE.md) |
/api/indicadores-apuracoes-empresas/{id}/ |
GET/PATCH | preenchimento manual do honorario de uma empresa com honorario_nao_encontrado=True (código não casou com a planilha de Honorários Por Cliente); zera essa flag e recalcula o colaborador |
/api/indicadores-apuracoes-respostas/{id}/ |
GET/PATCH | edição de uma resposta de critério (SIM/NÃO/NÃO FAZ/NÃO SE APLICA) já existente; recalcula o colaborador |
/api/indicadores-apuracoes-respostas/aplicar-em-lote/ |
POST | {resposta_ids, valor} — aplica o mesmo valor a várias respostas de uma vez (seleção múltipla da tela de revisão), recalculando todos os colaboradores afetados |
Frontend consumindo a API
static/js/api.js é a base de tudo: pidApiRequest(path, options) faz fetch com credentials:"include", injeta X-CSRFToken (lendo o cookie csrftoken, buscando-o via /api/auth/csrf/ primeiro se ainda não existir) em métodos não seguros, e redireciona pra index.html em 401 por padrão (redirectOn401: false para os poucos casos onde 401 é esperado, como o próprio login).
access.js expõe pidGetMe() — chamada única e cacheada por página (pidMePromise) para GET /api/me/. Cada arquivo controlador (account.js, favorites.js, widgets.js, profiles.js, users-admin.js, calendar-individual.js, links-ferramentas.js, acessos-gerais.js, ramais-lookup.js) chama pidGetMe() no início do seu próprio DOMContentLoaded, mas como todos rodam antes do primeiro await resolver, a promise cacheada garante uma única requisição de rede por carregamento de página, não uma por arquivo.
pidApiRequest (em api.js) detecta body instanceof FormData e, nesse caso, não faz JSON.stringify nem define Content-Type manualmente — deixa o browser montar o multipart/form-data com o boundary certo. Usado pelo upload de icone em Links & Ferramentas e pelos dois arquivos anexados em "Nova Importação" de Plano de Saúde; todo o resto da API é JSON puro. A única resposta binária da API (/importacoes-plano-saude/{id}/gerar/, que devolve CSV/ZIP) não passa por pidApiRequest — usa um fetch manual dedicado (ver seção "Importação de Plano de Saúde").
pidApplyAccessVisibility não recalcula mais união de permissões no cliente — usa me.permissoes_efetivas, já unida no backend (permissoes_efetivas() em views.py).
Páginas
| Página | Papel |
|---|---|
index.html |
Login. POST /api/auth/login/. |
portal.html |
Shell principal: busca de aplicações, grade de favoritos, seção de Widgets. |
calendario-individual.html |
Agenda pessoal: grade mensal + modal de criar/editar compromisso. |
perfis-acesso.html |
CRUD de perfis de acesso (lista + edição com abas Permissões/Usuários do Escritório). |
usuarios.html |
CRUD de contas de usuário (lista + edição com checklist de perfis). |
links-ferramentas.html |
Grade de cartões de atalho para ferramentas externas; reordenar/incluir/remover só com apps["links-ferramentas-editar"] (ver docs/links-ferramentas-acessos-gerais/links-ferramentas-acessos-gerais.md). |
acessos-gerais.html |
Cadastro de acessos/logins compartilhados organizados em seções e linhas, com popup de detalhes; criar/editar/excluir seções e linhas só com apps["acessos-gerais-editar"] (ver docs/links-ferramentas-acessos-gerais/links-ferramentas-acessos-gerais.md). |
ramais.html |
Diretório de ramais internos, filtros por nome/departamento; adicionar/editar ramal, criar ausência e excluir só com apps.editar em ramais (ver docs/ramais/ramais.md). |
importacao-plano-saude.html |
Ferramenta de Utilitários: histórico + cadastro de regras de custeio por empresa (separado da execução) + nova importação (upload, só aplica uma regra já cadastrada) + revisão/geração do arquivo de lançamento de plano de saúde (ver portal_api/planos_saude/CLAUDE.md). |
custo-contratacao.html |
Ferramenta de Geradoc: formulário de simulação de custo de contratação (Empregado CLT) + painel colapsável de parâmetros fiscais, gera um PDF (ver portal_api/custo_contratacao/CLAUDE.md). |
indicador-desempenho.html |
Ferramenta de Geradoc: histórico de apurações + nova apuração (upload das 2 planilhas) + cadastro de critérios/percentuais + revisão/geração dos recibos em PDF do Indicador de Desempenho do Fiscontábil (ver portal_api/indicadores/CLAUDE.md). |
nao-conformidades.html |
Aplicação de Relatórios > Qualidade: gestão contínua das ocorrências/ações do Sigsistem, com dashboard e status interno de tratativa da Qualidade (ver portal_api/nao_conformidades/CLAUDE.md). |
dashboard-contabil.html |
Aplicação de Relatórios > Contabilidade: histórico de análises + nova análise (upload do PDF de Balancete + DRE) + revisão de achados de auditoria (com observações por conta) + botão "Gerar Dashboard" (relatório HTML autocontido, ver portal_api/dashboard_contabil/CLAUDE.md). |
O item "Calendário De Paula" no menu não é uma página local — é um <a href="#" id="calendario-depaula-btn"> (continua favoritável, já que ainda é um <a class="nav-item"> — ver docs/favoritos/favoritos.md) cujo clique é interceptado em sidebar.js (PID_CALENDARIO_DEPAULA_URL) pra abrir https://depaula-tvcorporativa.lovable.app/calendario num modal com <iframe> (#calendario-depaula-modal, presente em todo shell) em vez de navegar — mesmo padrão do modal "Novo Chamado" de Ramais (.ram-chamado-* em ramais.css/ramais.js), só que genérico o bastante (.iframe-modal-* em components.css) pra existir em todo shell, não só em ramais.html. Funciona porque esse host (mesmo domínio do "Novo Chamado") não bloqueia ser embutido via X-Frame-Options/CSP, ao contrário do Asana (ver docs/solicitacoes/solicitacoes.md) — só foi possível confirmar isso testando de fato, não é garantia geral por domínio. Não recriar um calendario.html interno sem confirmar com o usuário; o antigo foi removido de propósito.
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 → confirm-modal.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>). confirm-modal.js (pidConfirm(), ver "Modal de confirmação genérico" abaixo) é a única exceção que também é carregada em index.html (logo depois de api.js ali também) — é genérico o bastante pra fazer sentido em qualquer página, inclusive o login.
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).
Padrão de guarda por página: profiles.js, users-admin.js e widgets.js verificam a existência do elemento raiz da própria tela (#pa-list-view, #ua-list-view, #widgets-grid) e retornam cedo se não estiverem na página certa — por isso são incluídos em todo shell mesmo quando só uma página usa a parte de "controlador" (profiles.js também expõe funções de dados — pidFetchPerfis, pidFetchUsuarios, pidFetchCatalogo — reaproveitadas por users-admin.js).
Armazenamento
| Onde | O quê |
|---|---|
localStorage (pid_theme, pid_color_theme) |
Só preferência de tema — ver theme.js. |
| Postgres, via API | Tudo o mais: sessão (cookie do Django), usuários, perfis de acesso, favoritos, widgets, compromissos do Calendário Individual, notificações dispensadas. |
notifications.js usa uma lista mockada em memória (PID_NEW_TOOLS_NOTIFICATIONS) para os anúncios de "nova ferramenta" — os itens de compromisso vêm de /api/compromissos/ de verdade. O conteúdo das notificações continua mockado/derivado a cada carregamento, mas quais delas o usuário já dispensou (X individual ou "Limpar tudo") persiste por usuário via NotificacaoDispensada//api/notificacoes-dispensadas/ — por isso um item dispensado não reaparece depois de um reload.
Notificação de ferramenta é gateada por permissão (n.access, cada entrada de PID_NEW_TOOLS_NOTIFICATIONS) — decisão explícita do usuário, pra nunca anunciar uma aplicação que o usuário não pode acessar. pidNotifToolElegivel() reproduz o mesmo critério que já esconde o item correspondente no menu (pidApplyAccessVisibility em access.js): {type:"gerencia"} espelha o gate de gerencia_permissoes (usado por "Perfis de Acesso"/"Usuários", que não têm chave em permissoes_efetivas), {type:"module", module:"..."} espelha permissoes_efetivas[module].enabled (usado por "Calendário Individual"/"Widgets", que vivem dentro de calendario-individual/principal). O filtro roda uma vez, antes de montar toolNotifications — vale tanto pro sino quanto pro histórico, então uma notificação sem permissão nunca aparece em lugar nenhum, nem mesmo depois de dispensada. Ao adicionar uma entrada nova em PID_NEW_TOOLS_NOTIFICATIONS, sempre preencher access com o módulo/gate real da aplicação anunciada (ou omitir só se for algo que todo usuário autenticado pode ver, sem exceção).
Notificação de ferramenta expira em 10 dias (PID_NOTIF_TOOL_EXPIRA_DIAS, pidNotifToolExpirada(), comparando dataIso da entrada contra a data de hoje): passado esse prazo, notifications.js dispensa a notificação sozinho no próprio carregamento da página (POST /api/notificacoes-dispensadas/, mesma chamada de quando o usuário clica no X) — daí em diante ela segue as mesmas regras de qualquer notificação dispensada manualmente (some do sino, aparece no histórico, pode ser restaurada). toolNotificationsAgora (o recorte usado pro sino, tanto na carga inicial quanto depois de um "Restaurar") já exclui as expiradas por prazo — restaurar uma notificação de ferramenta com mais de 10 dias mantém o rastro no histórico mas não a traz de volta ao sino, mesmo espírito de "restaurar um compromisso antigo não garante reaparecer no sino" (ver abaixo). Ao adicionar uma entrada nova, usar dataIso no formato "AAAA-MM-DD" (não date pré-formatado como antes) — date/pidFormatNotifDate() derivam o "DD/MM" de exibição a partir dele, mesmo padrão já usado pelos eventos do Calendário Individual.
Histórico de notificações (botão "Histórico" ao lado de "Limpar tudo", em #notif-history-modal — presente nos 7 shells que têm o sino, tudo exceto index.html): não é um model novo, é uma segunda leitura da mesma tabela NotificacaoDispensada — o sino ativo mostra !dismissedIds.includes(id), o histórico mostra o inverso (dismissedIds.includes(id)). A diferença entre os dois pools de candidatos usados (notifications.js) é proposital: o sino usa pidEventosElegiveisAgora()/toolNotificationsAgora (só eventos com data >= hoje e notificar_em já atingido, capado em 5 pra não lotar o dropdown; só notificações de ferramenta dentro dos 10 dias de prazo), enquanto o histórico usa allEventNotifications/toolNotifications (todos os compromissos que o usuário pode ver e toda notificação de ferramenta que ele tem permissão de ver, sem o recorte de "agora") — um item dispensado pode não estar mais no recorte "elegível agora" (compromisso já passou, lembrete não bateu ainda depois de uma edição, ou notificação de ferramenta já passou dos 10 dias), mas ainda precisa aparecer no histórico. Cada item do histórico tem um botão "Restaurar" (DELETE /api/notificacoes-dispensadas/{notif_id}/, já existia como endpoint, só não tinha consumidor no frontend) que remove o registro de dispensa; a notificação só volta a aparecer no sino de fato se ainda estiver no pool "elegível agora" (restaurar um compromisso muito antigo, ou uma notificação de ferramenta com mais de 10 dias, não reaparece no sino — o histórico continua mostrando, já que sua lista não tem esse recorte).
Modelo de permissões (Perfis de Acesso)
O catálogo de módulos/aplicações do menu vive só no backend agora (portal_api/catalogo.py) e é buscado uma vez por profiles.js via GET /api/catalogo/ — não editar mais um PID_MODULES/PID_MODULE_APPS hardcoded no frontend; a edição correta é em catalogo.py. 11 das 14 seções do menu têm sub-aplicações configuráveis individualmente (portais, geradoc, relatorios, relatorios-gerenciais, utilitarios, integracoes, auditorias, solicitacoes, links-ferramentas, ramais, calendario-individual); as outras 3 (principal, calendario, administracao) só têm um toggle de módulo, sem filhos. Em auditorias, cada entrada pode ser um subgrupo com tools aninhadas (ex.: "Consultoria Tributária" → "Controle Simples Nacional") — categorias reais que agrupam ferramentas reais. Em ramais e em links-ferramentas, o mesmo formato de subgrupo é reaproveitado por outro motivo: cada subgrupo ali é uma subtela/aplicação real da seção (ex.: "Ramais"/"Telefones Externos" dentro de ramais; "Links & Ferramentas"/"Acessos Gerais" dentro de links-ferramentas), e as tools dentro de cada subgrupo não são aplicações de verdade — são os dois níveis de acesso (visualizar/editar) daquela subtela (ver "Padrão visualizar/editar" abaixo). Em calendario-individual, o único subgrupo (calendario-individual-eventos) tem uma única tool (calendario-individual-criar-evento) em vez de um par visualizar/editar — a visualização do módulo em si já é liberada a todo perfil (está em BASE_KEYS), só a criação/gestão de eventos corporativos e categorias é restrita (ver "Eventos Corporativos" abaixo). Em geradoc, simulacao-custo-contratacao e indicador-desempenho são dois apps flat (sem subgrupo), cada um com permissão de toggle único — mesmo espírito de importacao-plano-saude em Utilitários (quem tem acesso faz o fluxo inteiro, sem par visualizar/editar).
Formato de perfil no banco (PerfilAcesso.permissoes, JSONField):
{
"<moduleKey>": { "enabled": true, "apps": { "<appKey>": true } }
}
Um usuário pode estar vinculado a mais de um perfil (M2M Usuario.perfis). A união de permissões (.some(...)/any(...) sobre todos os perfis vinculados) agora é calculada no servidor, em permissoes_efetivas() (portal_api/views.py) — o frontend só consome o resultado já pronto via me.permissoes_efetivas. As duas exceções ao padrão "checar permissoes_efetivas[chave].enabled":
data-section="perfis-acesso"edata-section="usuarios"(dentro de Administração) são gateados porgerencia_permissoes, não porpermissoes_efetivas.administracao.enabled.- O selo "Restrito" ao lado de Relatórios Gerenciais (
[data-restricted-badge]) é mostrado se algum perfil vinculado tivernome === "Diretoria"— um match de nome fixo, não uma flag de permissão.
Padrão visualizar/editar (Links & Ferramentas, Acessos Gerais, Ramais e futuros módulos parecidos)
Alguns módulos não são só "lista de aplicações que aparecem ou não no menu" — têm uma ou mais telas próprias com conteúdo administrável (adicionar/remover/reordenar), que pedem dois níveis de acesso, não um: visualizar (ver a tela) e editar (mudar o conteúdo dela). Em vez de criar um BooleanField dedicado por módulo (o que foi tentado numa rodada e revertido — não escala pra "várias aplicações com a mesma funcionalidade"), o padrão adotado é modelar cada aplicação como um subgrupo com dois tools (visualizar/editar) dentro do módulo em catalogo.MODULE_APPS — mesmo formato de subgrupo já usado em Auditorias/Ramais:
"links-ferramentas": [
{
"key": "links-ferramentas-cartoes",
"label": "Links & Ferramentas",
"tools": [
{"key": "links-ferramentas-visualizar", "label": "Visualizar"},
{"key": "links-ferramentas-editar", "label": "Editar (reordenar, incluir e remover cartões)"},
],
},
{
"key": "acessos-gerais",
"label": "Acessos Gerais",
"tools": [
{"key": "acessos-gerais-visualizar", "label": "Visualizar"},
{"key": "acessos-gerais-editar", "label": "Editar (criar/editar/excluir seções e acessos)"},
],
},
],
links-ferramentas nasceu com só um par visualizar/editar flat (sem subgrupo, já que só existia uma aplicação na seção); virou dois subgrupos quando "Acessos Gerais" foi adicionado como uma segunda aplicação dentro da mesma seção — a mesma evolução que ramais já tinha passado antes (ver "Navegação por abas em ramais.html" em docs/ramais/ramais.md). Cada chave de tool é prefixada com o nome da aplicação (links-ferramentas-visualizar, não só visualizar) porque permissoes[module_key]["apps"] é um dict achatado — todas as tools de todos os subgrupos do módulo compartilham o mesmo namespace, então chaves genéricas colidiriam entre as duas aplicações.
Isso reaproveita 100% a árvore de permissões que já existe (renderTree()/renderEntry()/renderLeaf() em profiles.js, sem nenhum código de UI novo) — na tela de edição de perfil, "Links & Ferramentas" aparece expansível com "Links & Ferramentas" e "Acessos Gerais" como subgrupos, cada um expansível de novo em "Visualizar"/"Editar", do mesmo jeito que "Auditorias" mostra "Consultoria Tributária" → "Controle Simples Nacional". No backend, a checagem usa Usuario.permissao_app(module_key, app_key) (união entre os perfis vinculados, mesma lógica de permissoes_efetivas()) e a classe genérica PermissaoApp(module_key, app_key) em permissions.py, instanciada por view — nenhuma subclasse nova é necessária pra outro módulo/aplicação adotar o mesmo padrão, só instanciar com outra app_key.
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.
Convenção pra aplicação nova que lide com dado sensível/pessoal (não dado de cliente comum): decisão explícita do usuário (rodada em que "Importação de Plano de Saúde - De Paula" foi criada, ver portal_api/planos_saude/CLAUDE.md) — daqui pra frente, toda aplicação nova adicionada ao catálogo que trate dado pessoal de colaborador do próprio escritório (em vez de dado de cliente, que é o caso comum do resto do Portal) deve nascer restrita só ao perfil "Inovação" (código 8), mesmo padrão já usado por "Não Conformidades" (permissoes["relatorios"]["apps"]["nao-conformidades"] = False no bloco de overrides) — nunca herdar a visibilidade ampla padrão de BASE_KEYS/SECTORAL_KEYS. Outros perfis que precisarem ganham acesso depois, manualmente, pela tela de Perfis de Acesso. Não retroagir essa convenção nas aplicações de toggle único que já existiam antes desta decisão (Importação de Plano de Saúde original, Simulação de Custo de Contratação, Indicador de Desempenho) — essas continuam com a visibilidade ampla de sempre, decisão explícita do usuário de não mexer no que já está em uso.
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.
Duas telas administrativas por cima desse modelo (popup "Nova Aplicação" de perfis-acesso.html, aba "Usuários do Escritório") estão documentadas em docs/perfis-usuarios/perfis-usuarios.md, não aqui.
Ajuda de aplicação ("Mais informações")
Botão "?" (.info-tooltip, components.css) ao lado do nome de uma aplicação — ao passar o mouse mostra a dica "Mais informações" (tooltip CSS puro, sem JS de posicionamento, já que a posição relativa ao próprio botão nunca varia); ao clicar, abre um modal com um texto livre descrevendo objetivo/processo/cuidados/resultado esperado daquela ferramenta. Visualizar é liberado a qualquer usuário autenticado; editar é restrito a quem tem o perfil "Inovação" vinculado — uma checagem de nome fixo (Usuario.eh_perfil_inovacao()/models.PERFIL_INOVACAO_NOME, mesmo padrão já usado pro selo "Restrito" de Relatórios Gerenciais, nome === "Diretoria"), não uma flag na árvore de permissões — decisão explícita do usuário, pra não precisar aparecer em Perfis de Acesso.
- Model (
AjudaAplicacao): chave naturalapp_key(mesma ideia deapp_id/notif_id/tipodeFavorito/NotificacaoDispensada/WidgetUsuario— sem FK praUsuario, é um texto compartilhado, igual pra quem abrir o modal) +texto(TextField,blank=True,validators=[validar_tamanho_texto_ajuda_aplicacao]— mesmo teto de 2.000.000 caracteres deAcessoGeral.observacoes, generoso o bastante pra várias imagens embutidas) +atualizado_em/atualizado_por.AjudaAplicacao.para_app(app_key)fazget_or_create— mesmo padrão de "singleton por chave, criado sob demanda" já usado emParametroFiscalCustoContratacao.atual()/EmpresaQuestor. - Endpoint
GET/PATCH /api/ajuda-aplicacoes/<app_key>/(ajuda_aplicacao_view, função simples — não éModelViewSet, mesmo estilo deparametros_fiscais_custo_contratacao_view): GET só exigeIsAuthenticated; PATCH também exigerequest.user.eh_perfil_inovacao(), senãoPermissionDenied.GET /api/me/ganhou o campoeh_perfil_inovacao(calculado no servidor, igual agerencia_permissoes) pra o frontend saber se mostra os botões "Editar" do modal. - Texto aceita imagens embutidas, igual às Observações de Acessos Gerais (pedido explícito do usuário) — mesmo mecanismo, reaproveitado:
<div contenteditable>no cliente com colar (Ctrl+V)/arrastar imagem (insertImageFile(), até 2MB), convertida em data URI e inserida viadocument.execCommand("insertImage", ...); sanitizado no servidor comnh3.clean()antes de salvar (AjudaAplicacaoSerializer.validate_texto). As constantes de allowlist do nh3 (RICHTEXT_ALLOWED_TAGS/_ATTRS/_SCHEMES,serializers.py) foram generalizadas (antes prefixadasACESSO_GERAL_*) pra serem compartilhadas pelos dois campos — mesmo allowlist estrito (texto básico +<img>, sem<a>/<script>/atributos de evento,data:liberado pra imagem embutida). A UI em si (.ajuda-modal__editor/.ajuda-modal__editor-hintemcomponents.css) é uma reimplementação de.ag-richtext(não um reaproveitamento direto), porque este modal pode aparecer em qualquer página, e.ag-richtext/acessos-gerais.jssão escopados só aacessos-gerais.html. - Frontend (
static/js/ajuda-aplicacao.js, reusável — igual ao espírito dedual-select.js):pidCriarBotaoAjuda(botaoId, appKey, tituloApp)liga o clique de um botão já existente no HTML da página; o modal em si (#ajuda-aplicacao-modal) é criado sob demanda e injetado emdocument.bodypelo próprio JS (não precisa ser hand-authored em cada página) e reaproveitado por todos os botões dela — só um pode estar aberto por vez. Texto em modo leitura viainnerHTML(seguro porque já vem sanitizado do backend, mesmo raciocínio de.ag-view-observacoes); quem temeh_perfil_inovacaovê um botão "Editar" que troca pro editor rico in-place (mesmo padrão "Cancelar"/"Salvar" já usado em "Editar Ausência" de Ramais). - Confirmação ao sair sem salvar (pedido explícito do usuário): enquanto o modal está em modo edição (
modal.dataset.editing = "true", setado porentrarEdicao()/limpo porrenderVisualizacao()), fechar o modal por qualquer caminho — botão "Fechar", clicar fora (overlay) ou "Cancelar" — disparaawait pidConfirm("Sair sem salvar as alterações?", { perigoso: true })(ver "Modal de confirmação genérico" abaixo); só fecha/descarta se confirmado. "Salvar" nunca pede confirmação (não há o que descartar).pidFecharAjudaAplicacaoModal()é a única função que fecha o modal de fato (agoraasync), então o botão "Fechar" e o clique no overlay (que já chamavam essa função) ganharam a checagem de graça; só "Cancelar" precisou de uma checagem própria antes de chamarrenderVisualizacao().
Modal de confirmação genérico (nunca window.confirm/window.alert)
static/js/confirm-modal.js (incluído logo depois de api.js em todo shell, inclusive index.html) — pidConfirm(mensagem, opcoes) devolve uma Promise<boolean> (true = confirmado, false = cancelado ou fechado clicando fora), num modal .modal-overlay/.modal-card no padrão visual do Portal, nunca o diálogo nativo do navegador. Decisão explícita do usuário: o window.confirm() nativo do Chrome mostra o IP/porta do servidor na barra de título do popup e quebra a identidade visual do app — todo popup novo deve usar este modal em vez disso.
opcoes(todas opcionais):titulo(default "Confirmar ação"),textoConfirmar/textoCancelar(defaults "Confirmar"/"Cancelar"),perigoso(troca o botão de confirmar pra.btn-danger-outline, mesmo estilo já usado em ações destrutivas como "Excluir selecionadas", em vez do.btn-solidpadrão).- Único modal (
#pid-confirm-modal), criado sob demanda e reaproveitado — empilha por cima de qualquer modal já aberto via.modal-overlay--top(components.css, sóz-indexmaior que o.modal-overlaypadrão), então não precisa fechar o modal atual antes de perguntar; o modal por trás continua visível (dimmed), só o de confirmação recebe o clique. - Reaproveita
.modal-card__title/.modal-card__subtitle(components.css, já genéricos) pro título/mensagem — não precisou de classes novas de texto, só.confirm-modal__cancelar-btn/.confirm-modal__confirmar-btncomo seletores de DOM pro próprioconfirm-modal.js. pidAlert(mensagem, opcoes)é a contrapartida prawindow.alert()— mesmo modal, um único botão (default "OK", sem "Cancelar"), devolvePromise<void>. Compartilha a mesma instância de#pid-confirm-modal/pidConfirmOuAlerta()internamente —pidConfirm/pidAlertsó chamam essa função commodoAlertadiferente.- Todo
window.confirm()/window.alert()do app foi migrado prapidConfirm()/pidAlert()(rodada de varredura completa, pedido explícito do usuário — "migre as demais"):acessos-gerais.js(excluir acesso, excluir seção),calendar-individual.js(excluir compromisso),links-ferramentas.js(remover link, erro ao favoritar),importacao-plano-saude.js(excluir regra de custeio, remover linha, reverter alteração, excluir importação — individual e em lote —, e o "Fechar sem salvar" do Cadastro de Regras, que tinha um modal bespoke próprio pra esse mesmo motivo —#ips-regracad-confirm-fechar-modal, criado numa rodada anterior só porquewindow.confirm()mostra a URL do servidor; removido e substituído porpidConfirm()nesta rodada, consolidando os dois em um único componente),ramais.js(remover telefone externo, remover função de telefonia, deletar ausência, remover ramal, erros),profiles.js(excluir perfil, erro ao salvar permissão),indicador-desempenho.js(excluir apuração/critério/registro de percentual/departamento),users-admin.js(excluir departamento/usuário, ativar/desativar usuário, avisos de "não pode excluir/desativar a si mesmo"). Ação destrutiva (excluir/remover/deletar) sempre usa{ perigoso: true }; ações não-destrutivas (reverter, ativar/desativar) não. - Não migrado:
window.prompt()emusers-admin.js(renomear departamento,data-dep-edit) — é um tipo de popup diferente (pede texto, não só confirmar/cancelar) e não tinha um componente equivalente pronto; fica pra uma rodada futura se for pedido, junto com um modal de input genérico. - Só ligado em Importação de Plano de Saúde por ora (
ips-ajuda-btnemimportacao-plano-saude.html, ao lado do<h2>dentro de.ips-title-row) — o mecanismo (model/endpoint/JS) já é genérico o bastante pra outra aplicação nova só precisar do botão+tooltip no HTML e uma chamada apidCriarBotaoAjuda(), sem nenhum código novo no backend. - Texto inicial de Importação de Plano de Saúde já estruturado (objetivo/como funciona/cuidados necessários/resultado esperado, em HTML simples —
<p>/<strong>/<ul>/<li>) e salvo direto no banco, pronto pra revisão/edição do usuário pela própria tela (perfil Inovação).
A inativação/reativação de usuário (is_active, incluindo o desvínculo automático de perfis/liderança), a Liderança (gerente/coordenador) e o bug do seed_portal.py não resetar mais nome de um perfil já existente (ver [[feedback_seed_nao_reseta_gabriel_bruno]] na memória) estão documentados em docs/perfis-usuarios/perfis-usuarios.md.
Favoritos
Grade de aplicações favoritadas na tela Principal (portal.html) — o usuário marca itens do menu como favoritos e reordena os cards por drag-and-drop. O ID de cada favorito é derivado da própria estrutura do menu, não de um cadastro à parte.
Ver docs/favoritos/favoritos.md.
Calendário Individual e Widgets
Agenda pessoal de cada usuário (compromissos privados, de departamento ou de todos), com eventos corporativos, feriados nacionais/estaduais do Paraná e lembretes calculados em horário comercial. Inclui também o sistema genérico de widgets configuráveis da tela Principal (drag-and-drop, redimensionamento).
Ver docs/calendario-individual/calendario-individual.md.
Links & Ferramentas / Acessos Gerais
Duas aplicações na mesma seção do menu: "Links & Ferramentas" é uma grade de cartões de atalho para ferramentas externas; "Acessos Gerais" é um cadastro de logins/acessos compartilhados da equipe, organizado em seções e linhas.
Ver docs/links-ferramentas-acessos-gerais/links-ferramentas-acessos-gerais.md.
Ramais
Diretório de ramais internos — mescla automaticamente todo colaborador ativo (a partir do cadastro de Usuários) com linhas avulsas (telefone de sala, recepção etc.). Inclui também Telefones Externos, Funções de Telefonia e controle de ausência.
Ver docs/ramais/ramais.md.
Solicitações
Os itens do menu "Solicitações" não são telas próprias — cada um é um link externo (hoje, um formulário do Asana) aberto em nova aba; não dá pra embutir em iframe porque o Asana bloqueia.
Ver docs/solicitacoes/solicitacoes.md.
Simulação de Custo de Contratação (Geradoc)
Calcula o custo de contratar um Empregado CLT (v1: só essa modalidade) a partir de tabelas fiscais de INSS/IRRF editáveis pelo banco, e devolve um PDF pronto pra enviar ao cliente.
Ver portal_api/custo_contratacao/CLAUDE.md.
Indicador de Desempenho (Geradoc)
Apuração mensal do indicador de desempenho do Fiscontábil (departamentos Contábil/Fiscal), a partir de planilhas do Tareffa (Serviços/Honorários), com critérios/percentuais configuráveis pela própria tela e geração de recibo em PDF por colaborador.
Ver portal_api/indicadores/CLAUDE.md.
Importação de Plano de Saúde (Utilitários)
Importa o relatório de faturamento de uma operadora de plano de saúde/odontológico (Amil, Unimed, SulAmérica...) e gera o arquivo de lançamento no leiaute do Questor, com auditoria do que não pôde ser casado automaticamente contra a planilha padrão.
Ver portal_api/planos_saude/CLAUDE.md.
Não Conformidades (Relatórios)
Gestão contínua das ocorrências e ações do Sistema de Gestão da Qualidade (Sigsistem, ISO 9001) — evolui uma skill que gerava um Excel estático sob demanda pra uma ferramenta persistente: cada ocorrência/ação é upsertada por código a cada importação, com um status interno de tratativa da Qualidade que reabre automaticamente quando algo muda desde o último tratamento, mais um dashboard com motivos de abertura e clientes/colaboradores com maior incidência.
Ver portal_api/nao_conformidades/CLAUDE.md.
Dashboard Contábil (Relatórios > Contabilidade)
Otimiza a conferência de balancetes hoje feita manualmente pelo Fisco/Contábil (ITD-FISCO-7513): o contador anexa o PDF de Balancete + DRE (modelo Questor, mesmo relatório hoje enviado ao cliente), a ferramenta extrai as contas e roda um motor de regras de auditoria (saldos negativos, contas transitórias/genéricas com saldo, contas que deveriam ficar zeradas, débito ≠ crédito, variações atípicas mês a mês contra o histórico já processado no Portal), com revisão de achados e observações por conta antes da conclusão. O botão "Gerar Dashboard" gera um relatório HTML autocontido (indicadores financeiros, gráfico de evolução, DRE/Balancete agrupados, observações do contador — com a marca do escritório, não a "P.I.D." do Portal), com exportação de Balancete/DRE em XLSX a partir dele. Ainda fora de escopo: consolidação entre várias empresas/competências ao mesmo tempo (substituiria o BI Contábil por completo).
Ver portal_api/dashboard_contabil/CLAUDE.md.
CSS — organização entre arquivos
| Arquivo | Contém |
|---|---|
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) e .pid-icon-eye/pidIconBlink (piscar de olho do ícone "P.I.D.", reaproveitado pela sidebar e pelo login — ver "Ícone do login e da sidebar são clicáveis" 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 11 páginas (perfis-acesso.html, usuarios.html, links-ferramentas.html, acessos-gerais.html, ramais.html, calendario-individual.html, importacao-plano-saude.html, custo-contratacao.html, indicador-desempenho.html, nao-conformidades.html, dashboard-contabil.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, + 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), .dual-select/.dual-select__* (vinculação em duas tabelas — não vinculados/vinculados, ver docs/perfis-usuarios/perfis-usuarios.md — usada em usuarios.html e no modal "Gerenciar Usuário" de todo shell), .info-tooltip/.info-tooltip__*/.ajuda-modal__* (botão "?" + tooltip + modal de "Mais informações", ver seção própria abaixo — usados por static/js/ajuda-aplicacao.js) e .modal-overlay--top (empilha um modal por cima de outro já aberto — usado só pelo modal de confirmação genérico, static/js/confirm-modal.js, ver "Modal de confirmação genérico" abaixo). |
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. O topbar da tela inicial não tem mais título/slogan nenhum (<h1 id="portal-title"> — chegou a existir brevemente com o slogan "Grandes aplicações de todos os tamanhos" em fonte "Pinyon Script"/dourado, removido a pedido do usuário na mesma rodada; ver login.css abaixo pra onde o slogan acabou indo) — o <link> do Google Fonts em portal.html também foi removido junto, já que não sobrou nenhum uso de fonte customizada nessa página. |
login.css |
Só usado em index.html; .login-card__title ("Portal Interno da De Paula") usa a fonte "Bree Serif" (importada só nesta página). O slogan da marca ("Grandes aplicações de todos os tamanhos.", pid-marca-leiame.md tem esse e um segundo, "Ainda funciona. Agora pensa.", não usado em lugar nenhum) mora aqui, não no topbar de portal.html (onde chegou a existir e foi removido, ver widgets.css acima) — passou primeiro pelo rodapé do card (abaixo de "Esqueceu sua senha?...") antes de subir pra logo abaixo do título, posição atual (pedido explícito do usuário). .login-heading (display:flex; flex-direction:column; gap:var(--space-1)) agrupa .login-card__title e .login-slogan, com um espaçamento bem menor entre os dois (--space-1) do que o gap:var(--space-5) que .login-card usa entre seus próprios filhos diretos — sem esse agrupamento, o slogan ficaria longe demais do título pra parecer uma assinatura. .login-slogan usa a fonte script "Pinyon Script" (mesmo <link> do Google Fonts de .login-card__title, agora com as duas famílias), font-size:1.2rem (menor que o 1.3rem do título do card, pedido explícito do usuário) e cor dourada fixa #c6a24a (cor da marca, não um token de tema/--accent). O card do login é congelado escuro nos dois temas (--login-card-bg/--login-field-bg/--login-border/--login-text-*, definidos só em :root de tokens.css, nunca redefinidos em :root[data-theme="light"] — mesmo padrão de --sidebar-*, ver comentário ao lado deles em tokens.css) — pedido explícito do usuário; a logo dentro dele também não alterna mais por tema (ver "Ícone do login e da sidebar são clicáveis" acima — decisão de uma rodada seguinte, revertendo a alternância que existia antes). Só o fundo da própria página ao redor do card (.login-page) continua acompanhando o tema claro/escuro, mas de formas diferentes em cada um: no escuro (padrão), background: linear-gradient(var(--overlay-scrim), var(--overlay-scrim)), radial-gradient(circle at 20% 20%, rgba(var(--accent-rgb), 0.25), transparent 45%), var(--bg-canvas) — um scrim escuro + um brilho radial na cor do tema sobre o --bg-canvas escuro, pensados pra dar profundidade; no claro, :root[data-theme="light"] .login-page zera o scrim (deixava tudo acinzentado/amarronzado sobre um fundo já claro) e refinou o resto em duas rodadas: primeiro só background: var(--bg-canvas) liso (pedido explícito do usuário, "mesmo tom do fundo da tela principal"); depois, a pedido do usuário de novo ("tom de roxo um pouco mais claro e o fundo branco levemente escurecido"), voltou a ter um brilho radial (rgba(accent, 0.12) — bem mais sutil que o 0.25 do tema escuro, um glow forte fica turvo sobre fundo claro) sobre uma cor base levemente mais escura que o --bg-canvas puro (#f3f1f7 → #ece7f2, só nesta tela — não altera o token --bg-canvas, então o resto do Portal no tema claro continua com o tom original). |
links-ferramentas.css |
.lf-* — só usado em links-ferramentas.html. |
acessos-gerais.css |
.ag-* — só usado em acessos-gerais.html. |
ramais.css |
.ram-* — só usado em ramais.html; 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). |
ramais-lookup.css |
.ram-lookup-* — modal de consulta rápida de ramais (ver docs/ramais/ramais.md), usado em portal.html/links-ferramentas.html/calendario-individual.html. Tabela autocontida (não reaproveita .pa-table porque essas 3 páginas não carregam perfis-acesso.css). |
importacao-plano-saude.css |
.ips-* — só usado em importacao-plano-saude.html; carrega perfis-acesso.css também, pra reaproveitar .pa-table/.pa-tabs/.pa-table-wrap na tabela editável da revisão e nas abas. |
custo-contratacao.css |
.cc-* — só usado em custo-contratacao.html. |
indicador-desempenho.css |
.ind-* — só usado em indicador-desempenho.html; carrega perfis-acesso.css também, pelo mesmo motivo de importacao-plano-saude.css (tabela/abas de revisão). |
nao-conformidades.css |
.ncf-* — só usado em nao-conformidades.html; carrega perfis-acesso.css também, pra reaproveitar .pa-table/.pa-tabs/.status-pill (modificadores --ok/--danger/--warning/--neutral novos, em cima do .status-pill já existente ali). |
dashboard-contabil.css |
.dc-* — só usado em dashboard-contabil.html; carrega perfis-acesso.css também, pra reaproveitar .pa-table/.pa-tabs na revisão de achados/Balancete/DRE. |
Ao adicionar uma tela nova que precise de modal, reuse .modal-overlay/.modal-card de components.css e só crie estilos de campo próprios se o formulário não for um caso simples de texto/select (que já tem equivalente em .pa-field ou .modal-field).
Animações
Três @keyframes genéricos em base.css (pidFadeIn, pidFadeSlideUp, pidScaleIn) — reutilizados via animation (não transition) em elementos que entram/saem do layout via hidden/display:none (modal, dropdown, .page-content a cada navegação, .login-card), porque só animation reinicia sozinho quando um elemento passa de display:none para visível; transition não anima essa troca (não há frame intermediário). Duração sempre curta (120–200ms) — pedido explícito do usuário: "fluidas, porém rápidas, otimizando o tempo". Botões (.btn-solid/.btn-outline/.btn-ghost/.btn-danger-outline/.icon-btn) ganharam transform: scale() no :active como feedback de clique.
Nenhuma dessas animações respeita prefers-reduced-motion — decisão deliberada do usuário ("as animações devem ignorar a preferência de não mostrar animações ou de acessibilidade do computador do usuário"), não um descuido. Não adicionar um bloco @media (prefers-reduced-motion: reduce) desativando isso sem confirmar de novo com o usuário, já que contraria um pedido explícito.
Animação de intro do login
Ao apertar "Entrar" com sucesso, index.html toca uma animação de marca em tela cheia (~6,2s: 500ms de fade + ~5,7s de animação) antes de navegar pra portal.html — pedido explícito do usuário, inspirado num arquivo pid-intro-escuro.html que ele forneceu (mesma pasta de origem dos SVGs da marca "P.I.D.", C:\Users\Depaula\Documents\Projetos\LOGOS\pid-marca).
Origem do arquivo — por que não foi só copiado: pid-intro-escuro.html não é HTML/CSS simples, é um bundle auto-contido de um editor de "Design Canvas" (Anthropic) — um <script type="__bundler/manifest"> com um JSON mapeando uuid → recurso ({mime, compressed, data}, data sendo base64 de um gzip), incluindo o JSX fonte de verdade (pid-intro.jsx, a composição em si) e a biblioteca de motion (animations-v3.jsx, easing/interpolação), montados em tempo real por um runtime React + motor de composição por "tempo autorado" (useComposition/CompositionStage) que não faz sentido carregar em produção (pesado, e pensado pra edição/export de vídeo, não pra rodar dentro de uma página de login). A coreografia foi extraída (decodificado gzip+base64 por uuid, script Python ad-hoc) e portada fielmente pra vanilla JS/CSS — mesmas durações de cena, mesmas curvas de easing (hand-rolled, estilo Popmotion) e mesmo timing de piscada dos olhos do original; só o destino final do "voo" do ícone foi recalibrado (ver "Cena Portal" abaixo).
As 4 cenas (PID_INTRO_CUES/PID_INTRO_TOTAL em login-intro.js — Build:0, Face:1, Wordmark:2, Portal:4.8, total 5.7): durações aceleradas em relação ao arquivo original (Build/Face de 2,6s/2s pra 1s cada, Portal de 2,3s pra ~0,9s — pedido explícito do usuário, "acelere um pouco a velocidade na qual a logo é construída"), exceto Wordmark (2,8s, igual ao original) — "a parte onde aparece o nome do portal deve permanecer a mesma", pedido explícito também. Os deslocamentos internos de cada cena (quando cada elemento começa/termina de animar) foram reproporcionados pra caber na duração nova, não são mais os valores originais do arquivo — só a cena Wordmark manteve os originais, já que a duração dela não mudou.
- Build (1s): o contorno do corpo do ícone se desenha (
stroke-dashoffset,pathLength="100"normaliza o path pra unidades 0–100 independente da geometria real), preenche a cor, a aba dourada "cai" (easeOutBack, dá um leve overshoot) e a "tela"/rosto do ícone abre (scale+opacity). - Face (1s): os dois olhos aparecem com "pop" (
easeOutBack) e piscam uma vez (pidIntroBlinkAt()— uma janela de 220ms em que a escala vertical do olho vai de 1 a 0 e volta a 1, formando o fecha-e-abre; a mesma função é reaproveitada em 2 momentos diferentes agora, ver abaixo — o terceiro blink do original, na cena Portal, foi removido: a cena ficou curta demais pra caber um blink visível com o ícone já encolhendo/voando), o sorriso dourado se desenha (mesma técnica destroke-dashoffsetdo corpo). - Wordmark (2,8s, inalterada): o ícone desliza pra esquerda (
SHIFT_X=-100px) enquanto "P.I.D." (fonte "Space Grotesk" 700, cada letra com um "." dourado à parte) + uma régua dourada (scaleX) + a tagline "Portal Interno da De Paula" (fonte "DM Mono", uppercase, letter-spacing largo) aparecem — cada letra com seu próprio atraso escalonado (CUES.Wordmark + 0.25 + i*0.13). - Portal (~0,9s): a wordmark esmaece quase na hora (
PortalaPortal+0.3), o ícone encolhe (de 160px pra 40px — o mesmo tamanho de.sidebar__logo--icon, ver "Logos emstatic/img/" acima) e "voa" até o canto superior esquerdo em 0,5s (Portal+0.05aPortal+0.55), e o stage inteiro (ícone+wordmark) esmaece logo depois, sobrepondo o fim do voo (Portal+0.5aPortal+0.9) — sem pausa parada entre o ícone assentar e o fade começar (ajuste de uma rodada anterior, que já tinha comprimido essa cena de 2,3s pra ~0,95s; esta rodada só encurtou mais um pouco, até ~0,9s). Diferença deliberada em relação ao original (além do tempo): lá o destino é um pixel fixo dentro de um frame de vídeo de exportação 1920×1080 (logoX:-892, logoY:-496, coordenadas que não existem em página nenhuma); aqui,pidIntroCornerTarget()calcula o alvo em tempo real a partir dewindow.innerWidth/innerHeight, mirando um ponto perto do canto real da janela — a ideia de "o ícone termina indo pro cabeçalho/sidebar do app" só faz sentido revisitada assim, já que o original nunca foi pensado pra rodar dentro de uma página de verdade. - As duas piscadas do olho (
Face+0.8,Wordmark+1.1) e as três funções de easing usadas (easeOutCubic/easeInOutQuad/easeOutBack) seguem as fórmulas exatas extraídas deanimations-v3.jsx— ver o código delogin-intro.jsse precisar ajustar timing, não redesenhar do zero.
Fade de entrada, antes do ícone começar a se desenhar (pedido explícito do usuário — a primeira versão fazia o overlay aparecer de repente por cima do card ainda visível, "de um modo bruto"; a duração foi ajustada de 350ms pra 500ms numa rodada seguinte, "está muito rápido... só pra ficar mais fluído"): pidPlayLoginIntro() aplica .is-leaving no .login-card (login.css, opacity:0; transform:scale(0.98), transição de 500ms) e .is-visible no #login-intro (opacity:0→1, mesmos 500ms, PID_INTRO_FADE_MS) ao mesmo tempo — o card se dissolve enquanto o overlay (já na cor final sólida) sobe por cima, um cross-fade real, não uma troca instantânea. Só depois desses 500ms (setTimeout) é que o loop de requestAnimationFrame do ícone começa (start = performance.now() é atribuído só nesse momento, não antes).
Arquivos: static/css/login-intro.css (só index.html) — formas idênticas às já usadas em pid-icone-escuro.svg/pid-logo-horizontal-escuro.svg (hex hardcoded — #7b5ba8/#c6a24a/#f2ede3, não são tokens de tema, são a paleta fixa da marca, mesmo espírito de .login-slogan já hardcodar #c6a24a); a exceção é o fundo do overlay, que reage ao tema (ver "Tema claro" logo abaixo) em vez de ser um hex fixo da marca. static/js/login-intro.js expõe pidPlayLoginIntro(onDone) (global, chamada só por auth.js) — monta o loop de requestAnimationFrame, calcula cada valor (bodyDraw, eyeL, shift, fly, ...) a partir de T (segundos decorridos desde o início do loop, via performance.now()) e escreve direto nos atributos/estilo dos elementos do overlay (#login-intro, já presente e hidden no HTML de index.html); chama onDone() quando T atinge PID_INTRO_TOTAL. Fontes "Space Grotesk" (peso 700) e "DM Mono" adicionadas ao mesmo <link> do Google Fonts de index.html, junto de "Bree Serif"/"Pinyon Script" já usadas ali.
Tema claro: o fundo do overlay (#121017, o valor fixo de --bg-canvas no tema escuro) e o texto do wordmark (.login-intro__letter/.login-intro__tagline, cor clara — pensados pra contrastar com um fundo escuro) só faziam sentido enquanto o overlay era sempre escuro (decisão original, pra login → animação → portal ler como uma coisa só). O usuário pediu que, no tema claro, o fundo da animação também acompanhasse o --bg-canvas claro (mesmo raciocínio já aplicado a .login-page em login.css) — o que por sua vez tornou o texto claro do wordmark ilegível contra um fundo claro. :root[data-theme="light"] overrides em login-intro.css cobrem os dois: .login-intro vira background: var(--bg-canvas) e .login-intro__letter/.login-intro__tagline viram cores escuras (#241c33/rgba(36, 28, 51, 0.62), os mesmos tons só invertidos). O ícone não precisou de override — o corpo roxo (#7b5ba8) tem contraste de sobra contra um fundo claro, e a "tela" do disquete (#241c33) já é escura por si só, então os olhos/sorriso continuam legíveis nos dois temas sem mudar nada. Nada disso afeta o tema escuro (padrão), que continua com os valores fixos originais.
Fluxo de navegação: auth.js, no sucesso do POST /api/auth/login/, chama pidPlayLoginIntro(() => { sessionStorage.setItem("pid_reveal_portal", "1"); window.location.href = "portal.html"; }) em vez de navegar direto — a navegação só acontece depois da animação inteira (não há como "pular" a animação hoje, nem foi pedido).
"A barra lateral surgindo da esquerda para a direita, e em seguida o resto da tela" (pedido explícito do usuário sobre como a tela Principal deveria surgir depois da animação, refinado duas vezes: a primeira versão só escondia .main-content e deixava a sidebar sempre visível desde o início, sem nenhuma entrada própria; a segunda versão deu à sidebar um fade + deslize sutil de 16px, considerado "ainda não satisfatório" — pequeno demais pra ler como "surgindo da esquerda pra direita"): static/js/portal-reveal.js (só portal.html, incluído defer logo depois de theme.js — mesmo padrão de "script que roda como IIFE de topo antes da primeira pintura pra evitar flash", ver "Ordem de <script>" acima) checa a sessionStorage marcada por auth.js; se presente, remove a marca (não sobrevive a um F5) e aplica duas classes em <html> antes do DOMContentLoaded: pid-entering-sidebar (esconde só .sidebar, opacity:0 + transform:translateX(-100%) — a largura inteira dela, não um deslize de poucos pixels, pra realmente ler como um slide de fora da tela) e pid-entering (esconde .main-content, o <div> que envolve topbar e conteúdo da página). As duas são removidas em sequência, não juntas: pid-entering-sidebar sai primeiro (120ms depois do DOMContentLoaded), a sidebar desliza da esquerda pra direita ao longo de 420ms (.sidebar em layout.css tem uma transition própria — opacity 420ms ease, transform 420ms cubic-bezier(0.16, 1, 0.3, 1), mais longa e com uma curva de "chegada" suave, não var(--transition-base) — genérica demais pra um movimento desse tamanho); só depois de esperar essa mesma duração (420ms) é que pid-entering sai, revelando o resto do app num fade, garantindo que as duas entradas não se sobreponham. Acessar portal.html direto (sem passar pelo login) nunca aciona nada disso, já que a sessionStorage só é setada no caminho de login bem-sucedido.
Testado pelo usuário em três rodadas (funcional) — ajustes até agora: a cor do overlay (era #241c33, virou #121017), o fade de entrada antes do ícone começar a se desenhar (não existia, overlay aparecia de repente; a duração foi ajustada depois de 350ms pra 500ms, "muito rápido... só pra ficar mais fluído"), a compressão da cena Portal (era 2,3s com pausa parada, foi pra 0,95s corrida numa rodada e depois ~0,9s), a aceleração de Build/Face (de 2,6s/2s cada pra 1s cada, Wordmark mantida em 2,8s de propósito) e a entrada da sidebar em portal.html (de um fade sutil de 16px pra um slide de fora da tela inteiro, translateX(-100%), 420ms). Ainda falta conferir o posicionamento do "voo" final em diferentes tamanhos de tela/com a sidebar colapsada.