433 lines
92 KiB
Markdown
433 lines
92 KiB
Markdown
# 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` |
|
||
|
||
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 seed_portal # recria os 8 perfis + usuários gabriel/bruno
|
||
python manage.py runserver
|
||
```
|
||
|
||
Abrir `http://localhost:8000/` — o Django serve `index.html` e as demais páginas do frontend diretamente (ver `config/urls.py`), então **não há mais um segundo servidor** (`python -m http.server`) para o frontend.Vamos desenvolver outra
|
||
|
||
Contas de demonstração (criadas por `seed_portal`, senha via `set_password` do Django — não é mais texto puro):
|
||
|
||
- `gabriel` / `gabriel` — perfil "Integração e Inovação" (código 8), o único com `gerencia_permissoes=True` (acesso total + gerencia Perfis de Acesso/Usuários) e o único com os `*-editar` de `links-ferramentas` (cartões e Acessos Gerais) e de `ramais` `True` (único perfil que pode reordenar/incluir/remover cartões em Links & Ferramentas, criar/editar seções e acessos em Acessos Gerais, e adicionar/editar ramal/criar ausência em Ramais, até que outro perfil seja liberado em Perfis de Acesso — ver "Modelo de permissões" abaixo).
|
||
- `bruno` / `bruno` — sem perfil vinculado, usado para testar o estado "sem acesso".
|
||
|
||
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 há um Postgres local acessível via `.env` — dá pra rodar `makemigrations`/`migrate`/`seed_portal`/`runserver` normalmente por aqui usando `.venv\Scripts\python.exe manage.py ...` (ou ativando o venv primeiro). 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 — ver `portal_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, ver `portal_api/custo_contratacao/CLAUDE.md`) — até uma rodada anterior também era usada no `sidebar__brand` dos 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 de `logo.png`, mas com o texto recolorido pra branco; gerada programaticamente a partir de `logo.png` (script Python com Pillow: qualquer pixel opaco quase-neutro/escuro — `max(r,g,b) < 70` e `spread(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, regerar `logo-branco.png` a partir do novo `logo.png` com 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 quando `data-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 de `pid-favicon.svg`/`pid-logo-horizontal.svg` abaixo).
|
||
- `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 com `pid-icone.svg` conforme `data-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, viewBox `300×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 usa `pid-icone-escuro.svg` mesmo), `pid-logo-horizontal.svg` (variante fundo claro da assinatura) e `pid-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 de `api.js` nos 10 shells): `.sidebar__brand` deixou 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 faz `preventDefault()`, adiciona `.is-blinking` em todo `.pid-icon-eye` dentro do link (pega os olhos das duas marcas — a visível e a escondida pelo crossfade, inofensivo já que a escondida tem `opacity:0`) e só navega pra `portal.html` depois 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), sem `preventDefault`/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** entre `pid-icone.svg`(claro)/`pid-icone-escuro.svg`(escuro) conforme `data-theme`, via `pidSyncLoginLogo()` em `theme.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-btn` sempre usa as cores/desenho de `pid-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`). |
|
||
|
||
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`):
|
||
```json
|
||
{
|
||
"<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"` e `data-section="usuarios"` (dentro de Administração) são gateados por `gerencia_permissoes`, não por `permissoes_efetivas.administracao.enabled`.
|
||
- O selo "Restrito" ao lado de Relatórios Gerenciais (`[data-restricted-badge]`) é mostrado se algum perfil vinculado tiver `nome === "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:
|
||
|
||
```python
|
||
"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.
|
||
|
||
**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 natural `app_key` (mesma ideia de `app_id`/`notif_id`/`tipo` de `Favorito`/`NotificacaoDispensada`/`WidgetUsuario` — sem FK pra `Usuario`, é 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 de `AcessoGeral.observacoes`, generoso o bastante pra várias imagens embutidas) + `atualizado_em`/`atualizado_por`. `AjudaAplicacao.para_app(app_key)` faz `get_or_create` — mesmo padrão de "singleton por chave, criado sob demanda" já usado em `ParametroFiscalCustoContratacao.atual()`/`EmpresaQuestor`.
|
||
- **Endpoint** `GET`/`PATCH /api/ajuda-aplicacoes/<app_key>/` (`ajuda_aplicacao_view`, função simples — não é `ModelViewSet`, mesmo estilo de `parametros_fiscais_custo_contratacao_view`): GET só exige `IsAuthenticated`; PATCH também exige `request.user.eh_perfil_inovacao()`, senão `PermissionDenied`. `GET /api/me/` ganhou o campo `eh_perfil_inovacao` (calculado no servidor, igual a `gerencia_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 via `document.execCommand("insertImage", ...)`; sanitizado no servidor com `nh3.clean()` antes de salvar (`AjudaAplicacaoSerializer.validate_texto`). As constantes de allowlist do nh3 (`RICHTEXT_ALLOWED_TAGS`/`_ATTRS`/`_SCHEMES`, `serializers.py`) foram **generalizadas** (antes prefixadas `ACESSO_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-hint` em `components.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.js` são escopados só a `acessos-gerais.html`.
|
||
- **Frontend** (`static/js/ajuda-aplicacao.js`, reusável — igual ao espírito de `dual-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 em `document.body` pelo 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 via `innerHTML` (seguro porque já vem sanitizado do backend, mesmo raciocínio de `.ag-view-observacoes`); quem tem `eh_perfil_inovacao` vê 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 por `entrarEdicao()`/limpo por `renderVisualizacao()`), fechar o modal por qualquer caminho — botão "Fechar", clicar fora (overlay) ou "Cancelar" — dispara `await 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 (agora `async`), 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 chamar `renderVisualizacao()`.
|
||
|
||
### 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-solid` padrã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-index` maior que o `.modal-overlay` padrã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-btn` como seletores de DOM pro próprio `confirm-modal.js`.
|
||
- **`pidAlert(mensagem, opcoes)`** é a contrapartida pra `window.alert()` — mesmo modal, um único botão (default "OK", sem "Cancelar"), devolve `Promise<void>`. Compartilha a mesma instância de `#pid-confirm-modal`/`pidConfirmOuAlerta()` internamente — `pidConfirm`/`pidAlert` só chamam essa função com `modoAlerta` diferente.
|
||
- **Todo `window.confirm()`/`window.alert()` do app foi migrado pra `pidConfirm()`/`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ó porque `window.confirm()` mostra a URL do servidor; removido e substituído por `pidConfirm()` 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()` em `users-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-btn` em `importacao-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 a `pidCriarBotaoAjuda()`, 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`.
|
||
|
||
## 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 10 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`) 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). |
|
||
|
||
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 de `stroke-dashoffset` do 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 (`Portal` a `Portal+0.3`), o ícone encolhe (de 160px pra 40px — o mesmo tamanho de `.sidebar__logo--icon`, ver "Logos em `static/img/`" acima) e "voa" até o canto superior esquerdo em 0,5s (`Portal+0.05` a `Portal+0.55`), e o stage inteiro (ícone+wordmark) esmaece logo depois, sobrepondo o fim do voo (`Portal+0.5` a `Portal+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 de `window.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 de `animations-v3.jsx` — ver o código de `login-intro.js` se 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.
|