68 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).
A numeração de rodada não é global: cada aplicação conta as próprias, e o mesmo número designa trabalhos diferentes em arquivos diferentes (ex.: "rodada 93" é uma coisa em plano.md e outra em portal_api/dashboard_contabil/CHANGELOG.md). Ao citar uma rodada, sempre nomear o arquivo — "ver rodada 45 em portal_api/indicadores/CHANGELOG.md", nunca só o número. Ver o topo de plano.md para o detalhamento.
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 |
| Relatório Contábil | portal_api/dashboard_contabil/ — CLAUDE.md (técnico), README.md, CHANGELOG.md |
| Conciliação de Fornecedores | portal_api/conciliacao_fornecedores/ — 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 |
| Temas Sazonais (marca P.I.D. sazonal — hoje só Halloween) | docs/temas-sazonais/ — temas-sazonais.md (técnico), README.md, CHANGELOG.md |
| Identidade visual (logos, marca P.I.D. na UI, animações genéricas, intro pós-login) — não é aplicação do menu, é camada transversal | docs/identidade-visual/ — identidade-visual.md (técnico), README.md, CHANGELOG.md |
Cada endpoint mora na doc da sua aplicação. A tabela de API mais abaixo tem só os transversais (auth, /api/me/, catálogo, perfis, usuários, favoritos, widgets, compromissos, notificações). Ao criar uma aplicação nova, documentar os endpoints dela na pasta dela.
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 15 páginas HTML (13 shells + index.html + o relatório do Relatório Contábil) (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: o logo cursivo "D De Paula Contadores" (logo.png/logo-branco.png/logo-mono.png), usado só nos documentos e PDFs que a aplicação gera, e a marca "P.I.D." (pid-*.svg), usada só na UI do Portal (favicon, login, sidebar). A separação é deliberada, não uma migração incompleta: um documento gerado (contrato, procuração, recibo do Indicador de Desempenho, PDF da Simulação de Custo, relatório do Relatório Contábil) é emitido como se o próprio escritório o tivesse gerado, e carrega a identidade dele perante o cliente. Não migrar o logo de um gerador de documento para "P.I.D." (nem o contrário numa tela do Portal) sem confirmar com o usuário. Ver [[feedback_logos_documentos_vs_portal]] na memória.
Ver docs/identidade-visual/identidade-visual.md para o inventário de cada arquivo, quem consome cada um, o crossfade de sidebar__brand e o mecanismo de piscar os olhos do ícone (que exige <svg> inline, não <img>).
Backend serve o frontend (mesma origem)
config/urls.py registra path("api/", include("portal_api.urls")) e, para cada uma das 15 páginas HTML do frontend (index.html mais os 14 shells — ver a tabela em "Páginas" abaixo), uma rota TemplateView que resolve o arquivo em templates/. O 15º template, dashboard-contabil-relatorio.html, não tem rota própria: é renderizado por uma view do Relatório Contábil, não navegável pela URL. 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 Django, portal_api/. Os models, serializers e views de todas as aplicações vivem nos arquivos compartilhados (models.py com ~2.600 linhas, views.py com ~5.100, serializers.py com ~2.800) — os pacotes abaixo contêm só lógica pura, sem ORM. Consequência prática: mexer nos models ou nas views de uma aplicação não carrega o CLAUDE.md dela automaticamente, porque esses arquivos não estão dentro do pacote. Ao trabalhar num model Contabil*, NaoConformidade*, Indicador*, ImportacaoPlanoSaude*, Ramal*, AcessoGeral* ou LinkFerramenta*, abrir a doc da aplicação correspondente (tabela em "Documentação dividida por aplicação" acima).
| Arquivo | Conteúdo |
|---|---|
models.py |
Todos os models do projeto. Os transversais: Usuario (AbstractUser + nome, M2M perfis, M2M departamentos, campos cadastrais opcionais codigo_folha/codigo_questor/codigo_tareffa/codigo_contabit/ramal, data_aniversario, lideranca e M2M liderados self-referential com related_name="lideres"; 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), PerfilAcesso (permissoes em JSONField + o booleano dedicado gerencia_permissoes), Departamento (só nome, cadastrado inline pela tela de Usuários, sem tela própria), AjudaAplicacao, CompromissoAgenda, Favorito, WidgetUsuario, NotificacaoDispensada. Os models de cada aplicação estão documentados na doc dela. |
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, nunca em static/js/profiles.js (que só cacheia o payload recebido). |
serializers.py |
Idem: todos os serializers. Os transversais são PerfilAcessoSerializer, DepartamentoSerializer, UsuarioResumoSerializer (id/nome/departamentos, usado nos dois lados de liderados e por /api/usuarios-resumo/), UsuarioSerializer (escrita) / UsuarioListSerializer (leitura, aninhados), CompromissoAgendaSerializer, FavoritoSerializer, WidgetUsuarioSerializer, NotificacaoDispensadaSerializer, AjudaAplicacaoSerializer. Também as constantes de allowlist do nh3 (RICHTEXT_ALLOWED_TAGS/_ATTRS/_SCHEMES), compartilhadas por todos os campos de texto rico do projeto. |
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. Nenhuma subclasse nova é necessária para uma aplicação adotar o padrão, só instanciar com outra app_key. |
views.py |
Idem: todas as views. As transversais são login_view/logout_view/csrf_view, me_view (usuário + permissoes_efetivas já unidas no servidor + lideranca/liderados + eh_perfil_inovacao), trocar_senha_view, usuarios_resumo_view, meus_liderados_view, departamentos_resumo_view, catalogo_view, ajuda_aplicacao_view, e os ModelViewSet de perfis/departamentos/usuários/compromissos/favoritos/widgets/notificações dispensadas. |
admin.py |
Django admin básico para todos os models (uso interno, não é a UI do portal). |
templatetags/contabil_extras.py |
Filtros de template (moeda/percentual/indice/competencia/mes_curto/moeda_av/percentual_av/numero_bruto) — único uso de template tags customizadas no projeto, só pelo relatório do Relatório Contábil. |
management/commands/seed_portal.py |
Cria 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. Não rodar neste ambiente — ver "Como rodar / testar localmente" acima. |
management/commands/seed_indicador_desempenho.py |
Popula o primeiro histórico do Indicador de Desempenho (7 IndicadorCriterio + 5 IndicadorPercentualTipo, idempotente). |
Pacotes Python puros (sem ORM), um por ferramenta — cada um com seu próprio CLAUDE.md/README.md/CHANGELOG.md:
| Pacote | Ferramenta |
|---|---|
planos_saude/ |
Pipeline de extração/casamento de "Importação de Plano de Saúde" (parsers por operadora, matcher, leiaute do Questor, regras de custeio). Serve as duas instâncias da ferramenta (clientes e De Paula). |
custo_contratacao/ |
"Simulação de Custo de Contratação" (Geradoc) — tabelas.py (faixas de INSS/IRRF), calculo.py, pdf.py via reportlab. |
indicadores/ |
"Indicador de Desempenho" (Geradoc) — tipos.py, leiaute.py (openpyxl), pipeline.py, entregas.py, calculo.py, recibo.py (PDF via reportlab), departamentos.py. |
nao_conformidades/ |
"Não Conformidades" (Relatórios > Qualidade) — leiautes dos exports do Sigsistem, diff.py (reabertura automática), classificacao.py, pipeline.py. |
conciliacao_fornecedores/ |
"Conciliação de Fornecedores" (Utilitários) — parser.py (razão do Questor em XLSX/CSV), motor.py (vínculos automáticos débito × crédito), alertas.py (situação/alertas recalculados a cada leitura), exportacao.py (XLSX). |
dashboard_contabil/ |
"Relatório Contábil" (Relatórios > Contabilidade) — parser.py (extração do PDF), regras.py (motor de auditoria), formula.py (avaliador de fórmula por ast), indicadores.py, chaves.py (chave natural de conta/linha, compartilhada por sincronização, observações e relatório), exportacao.py (XLSX), resumo_pdf.py. |
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" |
Esta tabela lista só os endpoints transversais. Os de cada aplicação moram na doc dela (ver "Documentação dividida por aplicação" acima): Links & Ferramentas/Acessos Gerais, Ramais/Telefones Externos/Funções de Telefonia, Importação de Plano de Saúde (as duas instâncias), Simulação de Custo de Contratação, Indicador de Desempenho, Não Conformidades e Relatório Contábil. Ao criar uma aplicação nova, documentar os endpoints dela lá, não aqui.
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").
Escape de HTML obrigatório (pidEscapeHtml, api.js): todo dado vindo da API, do usuário ou de arquivo anexado que entra em HTML montado por template string (innerHTML, insertAdjacentHTML) passa por pidEscapeHtml(texto), que trata & < > " ' e serve para conteúdo e atributo. Sem isso, um texto com HTML (título de compromisso, nome de link, observação, dado do arquivo da operadora) vira código executado no navegador de quem abre a tela, inclusive de outros usuários (XSS armazenado). Corrigido em todo o frontend na revisão de interface de 2026-09-25 (ver plano.md). Regras: escapar o dado, num único ponto (sem escape duplo), nunca o HTML que o próprio código monta (ícones, fragmentos); dado atribuído por .textContent/.value já é seguro e não se escapa; texto rico sanitizado no servidor com nh3 (Ajuda, observações de Acessos Gerais, Resumo do Fechamento) é HTML intencional e não se escapa. As funções locais de escape que ainda existem (pidDcEscapeHtml, pidNcfEscapeHtml, pidIndEscapeHtml, pidConcEscape, escapeHtml/escapeAttr do Plano de Saúde) delegam a ela; a versão antiga por textContent/innerHTML não escapava aspas e não deve voltar a ser usada.
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). |
importacao-plano-saude-de-paula.html |
Segunda instância da ferramenta acima, para o plano de saúde dos próprios colaboradores do escritório: tabelas, endpoints e permissão próprios, mas mesmo JS/CSS (parametrizados por window.PID_IPS_CONFIG) e mesmo pipeline de extração. Ver "Importação de Plano de Saúde - De Paula" em 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). |
conciliacao-fornecedores.html |
Ferramenta de Utilitários: histórico + nova conciliação (código da empresa com nome buscado no Questor + razão de fornecedores .xlsx/.csv) + detalhe com cards de alerta e tabela de fornecedores expansível, vínculo manual, validação e exportação XLSX (ver portal_api/conciliacao_fornecedores/CLAUDE.md). |
dashboard-contabil.html |
Aplicação de Relatórios > Contabilidade ("Relatório Contábil" na UI): 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 Relatório" (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, pid_seasonal_theme_opt_out, pid_seasonal_dead_spiders) |
Preferência de tema (theme.js) + preferências de Temas Sazonais (seasonal-theme.js, ver docs/temas-sazonais/). |
| 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 13 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: no hover mostra "Mais informações" (tooltip CSS puro, sem JS de posicionamento, já que a posição relativa ao próprio botão nunca varia); no clique abre um modal com um texto livre descrevendo objetivo/processo/cuidados/resultado esperado daquela ferramenta.
Visualizar é livre a qualquer autenticado; editar é restrito a quem tem o perfil "Inovação" vinculado — uma checagem de nome fixo (Usuario.eh_perfil_inovacao()/models.PERFIL_INOVACAO_NOME), 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. Mesmo padrão do selo "Restrito" de Relatórios Gerenciais (nome === "Diretoria").
- Model
AjudaAplicacao: chave naturalapp_key(mesma ideia deapp_id/notif_id/tipo— sem FK praUsuario, é um texto compartilhado) +texto+atualizado_em/atualizado_por.AjudaAplicacao.para_app(app_key)fazget_or_create, padrão "singleton por chave criado sob demanda" já usado emParametroFiscalCustoContratacao.atual(). - Endpoint
GET/PATCH /api/ajuda-aplicacoes/<app_key>/(função simples, nãoModelViewSet): GET exige sóIsAuthenticated, PATCH também exigeeh_perfil_inovacao().GET /api/me/expõeeh_perfil_inovacaopro frontend decidir se mostra o botão "Editar". - Texto aceita imagens embutidas:
<div contenteditable>com colar/arrastar imagem (até 2MB), convertida em data URI, sanitizada no servidor comnh3.clean(). As constantes de allowlist (RICHTEXT_ALLOWED_TAGS/_ATTRS/_SCHEMESemserializers.py) são compartilhadas por este campo, porAcessoGeral.observacoese porContabilApuracao.resumo_fechamento— allowlist estrito (texto básico +<img>, sem<a>/<script>/atributos de evento,data:liberado só pra imagem embutida). - Frontend (
static/js/ajuda-aplicacao.js, reusable no espírito dedual-select.js):pidCriarBotaoAjuda(botaoId, appKey, tituloApp)liga o clique de um botão já existente no HTML. O modal (#ajuda-aplicacao-modal) é criado sob demanda e injetado emdocument.bodypelo próprio JS, não precisa ser escrito à mão em cada página. Fechar em modo edição (por qualquer caminho: "Fechar", overlay ou "Cancelar") disparapidConfirm("Sair sem salvar as alterações?", { perigoso: true }). - Ligado hoje só em Importação de Plano de Saúde (
ips-ajuda-btn). O mecanismo já é genérico: outra aplicação precisa só do botão no HTML e de uma chamada apidCriarBotaoAjuda(), sem nenhum código novo no backend.
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 Promise<boolean> e pidAlert(mensagem, opcoes) devolve Promise<void>, os dois num modal .modal-overlay/.modal-card no padrão visual do Portal.
Decisão explícita do usuário: o window.confirm() nativo do Chrome mostra o IP/porta do servidor na barra de título e quebra a identidade visual do app. Todo popup novo usa este modal. Ver [[feedback_popups_no_padrao_do_portal]] na memória.
opcoes(todas opcionais):titulo(default "Confirmar ação"),textoConfirmar/textoCancelar,perigoso(troca o botão de confirmar pra.btn-danger-outline). Ação destrutiva sempre usa{ perigoso: true }; ação reversível (reverter, ativar/desativar) não.- Um único modal (
#pid-confirm-modal), criado sob demanda e reaproveitado;pidConfirm/pidAlertsão duas chamadas da mesma função interna commodoAlertadiferente. Empilha por cima de qualquer modal já aberto via.modal-overlay--top(só umz-indexmaior), então não é preciso fechar o modal atual antes de perguntar — o de trás continua visível, dimmed. - Reaproveita
.modal-card__title/.modal-card__subtitlepro título/mensagem; só.confirm-modal__cancelar-btn/.confirm-modal__confirmar-btnexistem como seletores de DOM. - Nenhum
window.confirm()/window.alert()restou no app. A única exceção é umwindow.prompt()emusers-admin.js(renomear departamento) — pede texto, e ainda não existe um modal de input genérico. Se for pedido, criar junto.
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.
Conciliação de Fornecedores (Utilitários)
O contador informa o código da empresa (nome buscado no Questor) e anexa o razão contábil de fornecedores exportado do Questor; a ferramenta vincula pagamentos e títulos de cada fornecedor por valor e data (inclusive combinações e parcelas) e aponta o que está pendente: títulos em aberto há mais de 2 meses, pagamentos sem título (divergência), diferenças de juros/desconto e conta transitória com saldo. Vínculo manual, validação por conta, histórico salvo e exportação XLSX. Restrita ao perfil Inovação.
Ver portal_api/conciliacao_fornecedores/CLAUDE.md.
Relatório Contábil (Relatórios > Contabilidade)
Chamado de "Dashboard Contábil" até uma rodada anterior — renomeado pra "Relatório Contábil" a pedido explícito do usuário, pra soar como um aliado do trabalho do contador em vez de mais um processo/sistema novo (rename só de rótulo visível: menu, título da página, cabeçalhos e o botão que gera o relatório; nomes técnicos internos — pasta do pacote, arquivos, classes de model, rotas — continuam dashboard_contabil/dashboard-contabil.*/Contabil*//contabil-*, sem nenhuma mudança).
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 ou Contabit, detectado sozinho, mesmo relatório hoje enviado ao cliente; no Contabit o código da empresa vem do nome do arquivo e a seção de indicadores fica oculta), 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 (a observação fica no histórico da empresa + conta e reaparece assinada nas competências seguintes, até ser encerrada) — cada conta/linha também ganha um checkbox de "validado" (marcador informativo de que o contador já conferiu aquele item) e um editor de observação inline na própria tabela (não mais um popup), com um toggle "mostrar ao cliente" que nasce desmarcado por padrão. O botão "Gerar Relatório" 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); os outros 12 shells (perfis-acesso.html, usuarios.html, links-ferramentas.html, acessos-gerais.html, ramais.html, calendario-individual.html, importacao-plano-saude.html, importacao-plano-saude-de-paula.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). |
conciliacao-fornecedores.css |
.conc-* — só usado em conciliacao-fornecedores.html; carrega perfis-acesso.css também, pra reaproveitar .pa-table/.pa-search/.status-pill. |
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), aplicados via animation e nunca via transition em elementos que entram/saem do layout por hidden/display:none (modal, dropdown, .page-content a cada navegação, .login-card) — só animation reinicia sozinha quando um elemento passa de display:none para visível; transition não anima essa troca, porque não há frame intermediário. Duração sempre curta (120–200ms), a pedido do usuário: "fluidas, porém rápidas, otimizando o tempo". Botões têm 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, já que contraria um pedido explícito.
Ao apertar "Entrar" com sucesso, toca uma animação de marca em tela cheia (~6,2s) antes de navegar para portal.html, e a tela Principal entra pela barra lateral primeiro. Ver docs/identidade-visual/identidade-visual.md para as 4 cenas, os timings, as curvas de easing e o mecanismo de revelação em portal-reveal.js.
Temas Sazonais
Troca sazonal da marca P.I.D. (logo, ícone da sidebar/login, favicon, intro de abertura pós-login) por uma variante temática durante uma janela de datas — hoje dois: Halloween (mascote "vampiro", 16 a 31/10) e Aniversário (56 anos da De Paula Contadores, mascote com balões, só 15/10) — as duas janelas nunca coincidem por desenho. Mecanismo pensado pra outro tema sazonal futuro reaproveitar o mesmo esqueleto. Durante o Halloween, clicar na logo dispara um "susto" (substitui a piscada normal) e há teias de aranha decorativas nos cantos do login/tela Principal (aranha "matável"); durante o Aniversário, clicar na logo dispara um "clique de festa" (pula + joga confete + língua-de-sogra, convive com a piscada normal em vez de substituí-la), mais um bolo com velinhas (apagáveis ao clicar) fixo no canto da tela Principal. Um toggle "Temas sazonais" no menu da conta deixa o próprio usuário desligar isso (preferência pessoal ou acessibilidade, ex.: aracnofobia). 100% runtime — fora da janela ativa (ou com o opt-out ligado), tudo volta sozinho ao P.I.D. de sempre, sem nenhuma ação manual.
Ver docs/temas-sazonais/temas-sazonais.md.