Commit inicial do Portal De Paula

This commit is contained in:
Gabriel 2026-08-18 10:57:39 -03:00
commit 3a6c3e1815
221 changed files with 32889 additions and 0 deletions

50
.gitignore vendored Normal file
View File

@ -0,0 +1,50 @@
# Ambiente virtual
.venv/
venv/
env/
# Variáveis de ambiente / segredos
.env
.env.*
!.env.example
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
# Django
staticfiles/
db.sqlite3
db.sqlite3-journal
*.log
local_settings.py
# Distribuição / empacotamento
build/
dist/
*.egg-info/
.eggs/
# Ferramentas de teste/cobertura
.coverage
.coverage.*
htmlcov/
.pytest_cache/
.tox/
# Editores / IDEs
.vscode/
.idea/
*.swp
*.swo
# Sistema operacional
.DS_Store
Thumbs.db
desktop.ini
# Configuração local do Claude Code
.claude/settings.local.json

581
CLAUDE.md Normal file
View File

@ -0,0 +1,581 @@
# 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.
## 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 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/`
`static/img/` tem três variantes do logo "D De Paula Contadores" (D em degradê dourado/marrom + texto), todas PNG com fundo transparente:
- `logo.png` — original, texto **preto**. Serve como fonte pra gerar as outras variantes e também é usada diretamente em `index.html` (login) quando `data-theme="light"` — o card do login usa `--bg-surface` (claro nesse tema), e o texto branco de `logo-branco.png` ficava ilegível contra ele.
- `logo-branco.png` — usada em `index.html` no login quando `data-theme="dark"` (default) e sempre no `sidebar__brand` dos 5 shells (a sidebar usa fundo frozen sempre escuro, independente do tema — ver "Sidebar" em `layout.css` — então não precisa alternar) — 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.
- A troca da logo do login por tema é feita em `theme.js` (`pidSyncLoginLogo()`): o `<img id="login-logo">` de `index.html` carrega os dois caminhos resolvidos por `{% static %}` em `data-logo-dark`/`data-logo-light`, e o JS só troca o `src` conforme `data-theme` atual — chamado no load e no evento `pid:theme-changed`.
- `logo-mono.png` — versão totalmente monocromática (D **e** texto em branco/cinza claro). Não usada em nenhum template hoje; existe como variante alternativa (útil se algum dia precisar de um logo "chapado" sem o dourado do D).
- `favicon.png` — só o D (sem o texto "De Paula Contadores"), quadrado, 192×192, usado como ícone da aba do navegador (`<link rel="icon" type="image/png">` no `<head>` das 7 páginas). Gerado a partir de `logo.png` com o mesmo filtro de `logo-branco.png` (pixel opaco quase-neutro/escuro — `max(r,g,b) < 70` e `spread(r,g,b) < 12` — é texto, não o D), mas em vez de recolorir esses pixels pra branco, eles são apagados (`alpha = 0`); o resultado é recortado pelo bounding box do que sobrou opaco e centralizado num canvas quadrado transparente (o D é mais alto que largo). Se o logo oficial mudar, regerar a partir do novo `logo.png` com o mesmo processo, não editar à mão.
Tamanho: `.sidebar__logo` é `width: 200px; height: auto` (era 56×56 fixo, esmagava o logo — a arte é bem mais larga que alta, ~1.41:1 — e ficava pequena demais); encolhe pra `44px` quando a sidebar colapsa (desktop `.is-collapsed` e o breakpoint mobile), senão o logo vaza da faixa de 76px.
### 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`), 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 seção "Liderança" abaixo) — `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), `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 seção própria abaixo), `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 seção "Ramais" abaixo), `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` (ferramenta "Importação de Plano de Saúde" em Utilitários, ver seção própria abaixo). |
| `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 seção própria abaixo). |
| `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`. |
| `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 seção "Indicador de Desempenho" abaixo. |
| `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 seção própria abaixo. |
| `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 seção própria abaixo. |
| `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 seção própria abaixo. |
### 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` + `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 seção "Liderança") |
| `/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 "Feriados no Calendário Individual" abaixo |
| `/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/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 "Inativar usuário" abaixo) |
| `/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 "Calendário Individual e Widgets") 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 "Eventos Corporativos" abaixo |
| `/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 "Favoritos" abaixo) |
| `/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 "Calendário Individual e Widgets" abaixo |
| `/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 seção "Links & Ferramentas" abaixo) |
| `/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 "Acessos Gerais" abaixo) |
| `/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 seção "Acessos Gerais" abaixo |
| `/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 seção "Ramais" abaixo |
| `/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 seção "Ramais") |
| `/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 seção "Importação de Plano de Saúde"); `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/{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/{id}/` | GET/PATCH | edição de uma linha da revisão (todos os campos, não só valores); mesma permissão da importação, sem conceito de "dono" |
| `/api/regras-custeio-plano-saude/`, `/api/regras-custeio-plano-saude/{id}/` | GET/POST/PATCH/DELETE | banco de regras de custeio salvas (`nome`+`operadora`+`tipos_lancamento`+`custeio_por_tipo`+`observacoes`, ver "Regras de custeio salvas" abaixo) — mesma permissão de toggle único da ferramenta; lista compartilhada, sem "dono" |
| `/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 seção própria abaixo |
| `/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 seção própria abaixo) |
| `/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 "Departamento organizacional" abaixo |
| `/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 "Departamento organizacional" abaixo |
| `/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 "Departamento organizacional" abaixo |
| `/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 "Empresas sem Honorário"/"Empresas Ajustadas Manualmente" abaixo) |
| `/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 "Checklist de revisão do RH" abaixo) |
| `/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 "Corrigir Responsável" abaixo) |
| `/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 seção própria abaixo). |
| `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 seção "Acessos Gerais" abaixo). |
| `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 seção "Ramais" abaixo). |
| `importacao-plano-saude.html` | Ferramenta de Utilitários: histórico + nova importação (upload) + revisão/geração do arquivo de lançamento de plano de saúde (ver seção "Importação de Plano de Saúde" abaixo). |
| `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 seção "Simulação de Custo de Contratação" abaixo). |
| `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 seção própria abaixo). |
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 "Favoritos" abaixo) 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 "Solicitações" abaixo) — 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 → 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>`).
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.
**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()` (só eventos com `data >= hoje` e `notificar_em` já atingido, capado em 5 pra não lotar o dropdown), enquanto o histórico usa `allEventNotifications` (todos os compromissos que o usuário pode ver, sem nenhuma das duas restrições) — um item dispensado pode não estar mais no recorte "elegível agora" (compromisso já passou, ou o lembrete não bateu ainda depois de uma edição), 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" (ex.: restaurar uma notificação de ferramenta sempre reaparece no sino, já que esse pool é estático; restaurar um compromisso muito antigo não reaparece, porque ele não está mais entre os 5 mais próximos elegíveis — 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`" abaixo). 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.
### Popup "Nova Aplicação" (gerenciar acesso por aplicação, entre perfis)
Botão `#pa-app-search-btn` ao lado de "Novo Perfil" (`perfis-acesso.html`) abre `#pa-app-search-modal` — o caminho inverso da árvore de permissões: em vez de abrir um perfil e marcar módulo por módulo, o usuário busca uma aplicação/ferramenta pelo nome e vê/gerencia **todos os perfis** que têm acesso a ela numa tabela só.
- **Fonte dos dados — 100% reaproveitado, sem endpoint novo**: `pidFlattenAplicacoes()` (`profiles.js`) achata `PID_MODULES` × `PID_MODULE_APPS` (já carregados de `GET /api/catalogo/` no load da página) numa lista plana de "aplicações" — uma por entrada de `MODULE_APPS`, seja ela um app simples ou um subgrupo com `tools` (mesma unidade usada em `renderEntry()` da árvore). A busca (`renderAppSearchResults()`) casa o termo contra o label da aplicação, o label do módulo **e** o label de cada `tool` aninhada — esse último é o que permite achar, por exemplo, "Controle Simples Nacional" (o `tool` real dentro do subgrupo "Consultoria Tributária" de Auditorias) mesmo a unidade selecionável sendo o subgrupo inteiro.
- **Painel de gerenciamento** (`renderAppManageTable()`): ao clicar num resultado, mostra uma tabela com uma linha por perfil (`profiles`, o mesmo array já carregado pela tela) e uma coluna de checkbox por `tool` — para app simples sem subgrupo, uma coluna única "Acesso". O cabeçalho de cada coluna usa `tool.label.split(" (")[0]` (corta o parêntese explicativo tipo "Editar (reordenar, incluir e remover cartões)" → "Editar"), sem precisar de um label curto dedicado no catálogo.
- **Salva na hora, por checkbox** (decisão explícita do usuário — sem botão "Salvar" no popup): cada `change` dispara `PATCH /api/perfis/{codigo}/` só com `{permissoes: perfil.permissions}` (`pidUpdatePerfilPermissoes`, PATCH parcial — o `ModelViewSet` já aceita, `PerfilAcessoSerializer` não exige os outros campos fora de `partial_update`). Erro de rede reverte o checkbox e o estado em memória, com um `alert()` simples (mesmo padrão de outros toggles imediatos do app, ex. inativar usuário).
- **Conceder acesso habilita o módulo automaticamente** (decisão explícita do usuário): se o checkbox marcado pertence a um módulo com `enabled=false` naquele perfil, o toggle também vira `perm.enabled = true` no mesmo PATCH — sem isso, o perfil ganharia a chave em `apps` mas o item continuaria escondido no menu (`access.js` esconde o `nav-group`/`nav-subitem` inteiro por `enabled`, não só por app). **Revogar não desabilita o módulo de volta** (outras aplicações dele podem seguir em uso por aquele perfil).
- Perfis inativos (`ativo=False`) aparecem na tabela com o selo `.status-pill--inativo` (mesmo componente da coluna "Status" de `usuarios.html`), sem serem excluídos da lista — nada nesse popup impede gerenciar o acesso deles.
## Inativar/reativar usuário (usuarios.html)
Usa o campo `is_active` que já vem de `AbstractUser` — não foi criado nenhum campo/migração novo, só exposto em `UsuarioSerializer`/`UsuarioListSerializer` e ligado na UI. `is_active=False` já é suficiente pro Django bloquear o acesso sozinho, sem nenhum código extra de autenticação:
- **Login novo**: `authenticate()` (usado em `login_view`) roda via `django.contrib.auth.backends.ModelBackend`, que internamente chama `user_can_authenticate()` e recusa (`None`) qualquer usuário com `is_active=False`, mesmo com a senha certa. Como isso faz `authenticate()` retornar `None` tanto pra senha errada quanto pra usuário inativo, `login_view` faz uma checagem manual **só no caminho de falha** (`Usuario.objects.filter(username=username, is_active=False).first()` + `check_password()`) pra devolver uma mensagem diferente ("Este usuário está inativo...", 403) só quando a senha bate mas a conta está inativa — sem essa checagem extra, qualquer tentativa com credenciais erradas ou inexistentes continua caindo no genérico "Login ou senha inválidos." (401), pra não revelar se um username existe.
- **Sessão já aberta**: também não precisa de nenhum middleware/permissão customizado — `ModelBackend.get_user(user_id)` (chamado pelo Django a cada request pra popular `request.user` a partir da sessão) também recusa usuários inativos, então na próxima requisição depois de desativado o usuário vira `AnonymousUser` automaticamente e `IsAuthenticated`/`PodeGerenciarPermissoes` já barram sozinhos. Ou seja: desativar alguém já derruba o acesso na mesma hora, não só impede o próximo login.
Na UI (`users-admin.js`/`usuarios.html`): coluna "Status" na lista (`.status-pill`/`.status-pill--ativo`/`.status-pill--inativo`, mesmo componente que já existia pro `PerfilAcesso.ativo`) e um botão de alternar (ícone de "power") na linha, ao lado de editar/excluir — `PATCH /api/usuarios/{id}/` com `{is_active: !atual}`, com confirmação via `window.confirm`. O checkbox "Usuário ativo" no formulário de edição faz a mesma coisa (útil quando já se está editando outros campos). Os dois lugares bloqueiam **auto-desativação** (mesmo padrão de guarda já usado pra "não pode excluir a si mesmo": checagem só no frontend, `id === me.id`) — no formulário isso aparece como o checkbox desabilitado (`disabled`) quando `account.id === me.id`, em vez de um alerta.
## Liderança (gerente/coordenador) e o modal "Gerenciar Usuário"
`Usuario.lideranca` (booleano) marca um usuário como gerente/coordenador de outros; `Usuario.liderados` é um M2M **auto-referenciado** (`"self"`, `symmetrical=False`, `related_name="lideres"`) — ou seja, "A lidera B" não implica "B lidera A". Exemplo: marcar `lideranca=True` em "debora" e incluir "gabriel" em `liderados` representa "debora é gerente de gabriel".
Dois lugares gravam essa mesma relação:
- **Tela de Usuários** (`usuarios.html`/`users-admin.js`, só quem tem `gerencia_permissoes`): seção "Liderança" no formulário de edição — checkbox "É gerente ou coordenador de outros usuários" (`#ua-lideranca`) libera (`hidden`) uma `.checklist-box` (`#ua-liderados-checklist`) com todos os outros usuários cadastrados (a própria conta sendo editada é excluída da lista — mesmo padrão de guarda "frontend-only" já usado pra "não pode excluir a si mesmo"/"não pode se auto-desativar", não há checagem equivalente no backend). Salvar envia `lideranca`+`liderados` (array de ids) no mesmo payload de `PATCH`/`POST /api/usuarios/`.
- **Modal "Gerenciar Usuário"** (`account.js`, disponível em todo shell via o item "Gerenciar Usuário" no dropdown da conta — substituiu o antigo botão direto "Alterar senha"): abre `#manage-account-modal`, que sempre tem um botão "Alterar senha" (que fecha esse modal e abre o `#password-modal` já existente, mesmo fluxo de antes) e, só se `me.lideranca` for `true`, uma segunda seção com a mesma `.checklist-box` de liderados — permitindo que o próprio gerente/coordenador se autogerencie sem precisar de acesso à tela administrativa de Usuários. Essa lista vem de `GET /api/usuarios-resumo/` (não de `/api/usuarios/`, que exige `gerencia_permissoes`) e salvar dispara `PATCH /api/me/liderados/`, que grava na mesma `Usuario.liderados` — `meus_liderados_view` recusa (403) se `request.user.lideranca` for `False`, já que só faz sentido pra quem tem o checkbox marcado.
**Busca por nome/departamento e layout em duas linhas**: `UsuarioResumoSerializer` (usado por `/api/usuarios-resumo/` e por `liderados` aninhado em `UsuarioListSerializer`/`/api/me/`) expõe também `departamentos` (nested `DepartamentoSerializer`), não só `id`/`nome`. Os dois pontos que renderizam o checklist de liderados (`users-admin.js`/`account.js`) usam isso pra: (a) cada item (`.checklist-item`) mostrar o nome e, numa segunda linha abaixo (`.checklist-item__info` empilha os dois em coluna), os departamentos do candidato unidos por vírgula (`—` se nenhum) — layout em duas linhas com quebra normal (não uma só linha com `text-overflow: ellipsis`) porque truncar cortava departamentos no meio quando o usuário tinha mais de um vinculado; (b) um campo de busca (`.checklist-search`, `#ua-liderados-search`/`#manage-account-liderados-search`) que filtra a lista pelo nome do usuário **ou** pelo texto dos departamentos (`String.includes`, case-insensitive) — qualquer um dos dois casa. A seleção de checkboxes é mantida em memória num `Set` (`lideradosSelecionados`, um por tela) em vez de lida direto do DOM, porque filtrar re-renderiza `innerHTML` e apagaria o estado de quem estivesse marcado mas momentaneamente fora do filtro; um listener de `change` delegado no container (`.checklist-box`) atualiza esse `Set` a cada clique, e o array final enviado à API vem de `Array.from(set)`, não de uma querystring nos checkboxes visíveis.
**"Marcar todos os resultados da busca"** (`.checklist-select-all`, `#ua-liderados-select-all`/`#manage-account-liderados-select-all`): opera só sobre o subconjunto **filtrado** pela busca no momento (não sobre todos os candidatos) — marcar adiciona o id de cada usuário filtrado ao `Set`, desmarcar remove; a cada re-render, o próprio estado desse checkbox é recalculado a partir do filtro atual (`checked` se todos os filtrados estão no `Set`, `indeterminate` se só parte, `disabled` se a busca não retornou ninguém), então ele reflete o resultado visível, não um total geral fixo.
`.checklist-box`/`.checklist-item` (+ `.checklist-item__nome`/`.checklist-item__departamento`/`.checklist-empty`/`.checklist-search`, `components.css`) é o componente genérico de "lista com checkbox, coluna secundária e busca" reaproveitado pelos três checklists de `usuarios.html` (Perfis de Acesso, Departamento, Liderados — os dois primeiros sem coluna/busca, só o layout base) **e** pelo checklist de liderados do modal "Gerenciar Usuário" — por isso mora em `components.css` (carregado em todo shell), não em `perfis-acesso.css` (só carregado em `usuarios.html`/`perfis-acesso.html`/`ramais.html`), onde vivia antes de existir um segundo consumidor fora dessas três páginas.
## Favoritos: como o ID de uma aplicação é derivado
`favorites.js` não depende de nenhum atributo `data-*` dedicado para identificar "o que é favoritável" — ele varre `.sidebar a.nav-item, .sidebar a.nav-subitem` e deriva um ID estável a partir da própria estrutura/texto do menu (`pidCollectFavoritableApps`), igual a antes da migração. Esse ID é o que vira `app_id` em `POST /api/favoritos/` e na URL de `DELETE /api/favoritos/{app_id}/`:
- Se o `<li>` do link já tem `data-section`, o ID é esse valor (ex.: `"ramais"`).
- Caso contrário (é um sub-item dentro de um `nav-group`), o ID é `"<data-section do grupo pai>__<slug do texto do link>"` (ex.: `"portais__portal-do-cliente"`).
O slug (`pidSlug`) normaliza acentos (NFD) e troca sequências de caracteres não `[a-z0-9]` por `-`. Se o texto de um label mudar, o `app_id` derivado muda junto (favoritos existentes referenciando o ID antigo deixam de casar).
**Reordenar os cards favoritos**: `Favorito.ordem` (`PositiveIntegerField`, `Meta.ordering = ["ordem", "id"]`) — mesmo padrão de `LinkFerramenta`/`WidgetUsuario`: `FavoritoViewSet.perform_create` atribui `ordem = max(ordem atual do usuário) + 1`, e reordenar é drag-and-drop nativo em `#app-card-grid` (`favorites.js`, `dragstart`/`dragover`/`drop`, `PATCH /api/favoritos/{app_id}/` só nos itens cujo `ordem` mudou) — mesma mecânica dos outros dois. `.app-card` inteiro é `draggable="true"` (não precisa de um handle separado como os widgets, já que não tem `resize` pra conflitar); o botão de remover (`.app-card__remove`) é `draggable="false"` pra não interferir.
## Calendário Individual e Widgets
Compromissos (`CompromissoAgenda`) têm um dono (`dono`, FK) e um campo `visibilidade` (`"somente_eu"`/`"departamento"`/`"todos"`, substituiu a antiga flag booleana `compartilhado_com_perfil` — perfil de acesso deixou de ser o critério de compartilhamento). `GET /api/compromissos/` (`CompromissoAgendaViewSet.get_queryset`) já retorna: (a) sempre os próprios compromissos do usuário; (b) todo compromisso com `visibilidade="todos"`, pra qualquer usuário do portal, sem checar perfil/departamento; (c) compromissos com `visibilidade="departamento"` só se `departamento_compartilhado` (FK, `on_delete=SET_NULL`) for um dos departamentos do usuário logado — a lógica de "visível para quem" mora só no backend. O campo `sou_dono` (calculado no serializer) substitui o antigo `ownerLogin === login` do cliente; só o dono edita/exclui (`CompromissoAgendaViewSet.get_object` levanta `PermissionDenied` se não for o dono tentando escrever).
Ao escolher `visibilidade="departamento"` no modal (`calendario-individual.html`/`calendar-individual.js`), um segundo campo aparece (`#ic-event-department-field`) pra escolher **qual** dos próprios departamentos do dono recebe o compartilhamento — decisão explícita do usuário, já que `Usuario.departamentos` é M2M (pode ter mais de um) e "meu departamento" sozinho seria ambíguo nesse caso. As opções desse `<select>` vêm de `me.departamentos` (adicionado a `/api/me/` só pra isso — antes esse endpoint não expunha os próprios departamentos do usuário logado, só o de outros via `UsuarioResumoSerializer`). `CompromissoAgendaSerializer.validate()` exige `departamento_compartilhado` quando `visibilidade="departamento"` e recusa qualquer departamento que não esteja entre os do próprio dono (`request.user.departamentos`) — mesmo que o cliente tente forçar um id de departamento alheio no payload; para as outras duas visibilidades, `departamento_compartilhado` é sempre zerado no servidor, ignorando o que vier no payload.
**Filtro por categoria e cores no calendário** (`.calendar-filters`/`.filter-chip` em `calendario.css` — já existiam no CSS sem nenhum consumidor antes desta funcionalidade): dentro de `.calendar-header`, ao lado do título do mês e do botão "Novo Compromisso" (mesma linha, não numa faixa própria abaixo — `flex-wrap: wrap` no header cobre o caso de não caber tudo numa linha só), uma barra de chips clicáveis (multi-seleção, sem exclusividade) filtra o que aparece no calendário por `pidEventoCategoria(ev)` (`calendar-individual.js`), que não é exatamente `ev.visibilidade` — um compromisso `"somente_eu"` que não é meu (`!ev.sou_dono`) só pode ter chegado pela regra de liderança abaixo, então vira a categoria `"equipe"`, distinta de `"somente_eu"` (meus próprios); e todo `ev.eh_evento` vira a categoria `"evento"` **antes** de qualquer outra checagem (ver "Eventos Corporativos" abaixo), mesmo já sendo sempre `visibilidade="todos"`. As 5 categorias (`somente_eu`/`departamento`/`todos`/`equipe`/`evento`) têm cor fixa própria (`.calendar-event--*` em `calendario.css`) e o chip ativo de cada uma usa a mesma cor — o próprio filtro funciona como legenda. `"somente_eu"` é a exceção: usa `--accent` (o tema de cor que o usuário escolheu, não uma cor fixa); as outras quatro usam tokens fixos novos (`--gold`, reaproveitado do antigo "compartilhado"; `--teal` e `--slate`, adicionados só pra isso em `tokens.css`; `--coral`, adicionado depois só pra `"evento"` — todos com variante mais escura no tema claro pro contraste do texto escuro fixo `#1a1721`) — escolhidos deliberadamente fora das 5 cores de tema selecionáveis (roxo/azul/verde/âmbar/rosa) pra nunca coincidir visualmente com o que `--accent` pode assumir. O chip "Minha equipe" (`#ic-filter-equipe`) só aparece (`hidden`) se `me.lideranca`; o chip "Eventos" (`data-filter="evento"`) é sempre visível, já que qualquer perfil pode ver eventos corporativos (só criar um é restrito). Por padrão todos os chips visíveis nascem `is-active` (mostra tudo que o usuário pode ver). Clique simples troca a seleção pra **só** aquele filtro (`activeFilters.clear()` + adiciona só o clicado), exceto se esse filtro já for o único ativo — nesse caso (`activeFilters.size === 1 && activeFilters.has(filtro)`) o clique volta pra visualização padrão (`selecionarTodosOsFiltros()`, todos os chips visíveis ativos), pra sempre existir um caminho de volta ao estado "ver tudo" sem precisar de Shift. `Shift`+clique acrescenta/remove esse filtro dos já selecionados (`event.shiftKey`, mesmo padrão de seleção de arquivos do SO) — é assim que dá pra combinar mais de uma categoria ao mesmo tempo.
**Eventos Corporativos** (`CategoriaEvento`, `CompromissoAgenda.eh_evento`/`categoria`/`local`/`modalidade`/`descricao`): compromissos com `visibilidade` em `departamento`/`todos` deixaram de ser livres pra qualquer usuário — criar ou editar um compromisso nesses dois níveis (o que inclui automaticamente todo `eh_evento=True`, já que a validação força `visibilidade="todos"` antes de checar permissão) agora exige `apps["calendario-individual-criar-evento"]` em `permissoes["calendario-individual"]` (`CompromissoAgendaSerializer.validate()`), permissão liberada só para "Integração e Inovação" no `seed_portal.py` por ora (mesmo cuidado de sempre: como `calendario-individual` está em `BASE_KEYS`, sem o override todo perfil nasceria podendo criar evento de departamento/todos e cadastrar categoria). Compromissos `"somente_eu"` continuam livres pra qualquer um, sem essa checagem.
- `CategoriaEvento` (`nome` único + `cor` hex, validada por `validar_cor_categoria_evento`) é um cadastro simples via `/api/categorias-evento/` — GET livre a qualquer autenticado (a cor/nome de uma categoria não é sigilosa), escrita restrita à mesma permissão acima. Não é uma lista fixa no código: quem tem a permissão cadastra categorias novas (ex.: "Reunião", "Treinamento") direto no modal de criar/editar compromisso (botão "+" ao lado do `<select>` de categoria, `#ic-event-categoria-add-btn`, que abre `#ic-categoria-modal`) — `seed_portal.py` popula `CATEGORIAS_EVENTO_SEED` como ponto de partida, mas a lista é editável dali em diante.
- `CompromissoAgenda.eh_evento` marca um compromisso como evento formal (não uma reunião pessoal marcada como "todos") — o checkbox correspondente (`#ic-event-eh-evento`, dentro de `#ic-event-eh-evento-field`) só aparece pra quem tem a permissão de criar evento; marcá-lo força a visibilidade pra "Todos" no próprio formulário. `local` (texto livre), `modalidade` (`presencial`/`remoto`/`hibrido`, `<select>` `#ic-event-modalidade`) e `descricao` (texto livre) são campos extras só relevantes pra evento, mas tecnicamente gravam em qualquer compromisso (o formulário só os expõe quando aplicável). No popup somente-leitura (`#ic-view-modal`), cada um aparece como campo próprio (`#ic-view-local-field`/`#ic-view-modalidade-field`/`#ic-view-descricao-field`), escondido (`hidden`) quando vazio — mesmo padrão dos demais campos condicionais desse popup (ver "Pill do compromisso" acima).
- Visualmente, um evento ganha um bucket de cor próprio (`--coral`) tanto no pill do calendário (`.calendar-event--evento`) quanto no chip de filtro "Eventos" — mesmo sendo sempre `visibilidade="todos"` por baixo, não se mistura visualmente com um "Todos" comum (ver parágrafo acima).
**Pill do compromisso: sempre "HH:MM Título", nada mais** — todo pill mostra só horário+título, nunca o nome do dono, pra manter o mesmo formato/tamanho independente da categoria ("simétrico", pedido explícito do usuário; a primeira versão acrescentava "— Nome do dono" direto no texto dos compromissos que não eram do usuário, o que descalibrava o visual porque nomes têm tamanhos bem diferentes). Todo pill é clicável (`cursor:pointer` na classe base `.calendar-event`, não só em `--somente-eu`): se `ev.sou_dono`, abre o modal de edição de sempre (`#ic-modal`, fecha também clicando fora — `event.target === modal`, mesmo padrão de `links-ferramentas.js`/`ramais-lookup.js`); senão, abre um modal novo, só leitura (`#ic-view-modal`/`openViewModal()`, mesmo fecha-ao-clicar-fora), com data/horário por extenso, `dono_nome` (rotulado "Agendado por:", não "Responsável" — mudança de nomenclatura pedida pelo usuário), o rótulo da visibilidade (`PID_IC_VISIBILIDADE_LABELS`, mapeia `ev.visibilidade` pro texto exibido nos chips) e, só quando `ev.visibilidade === "departamento"`, o nome do departamento (`ev.departamento_compartilhado_nome`, campo já vinha do serializer). É esse popup — não o texto do pill — que carrega toda a informação que antes tentava caber na própria pílula.
**Célula do dia com altura fixa, lista de compromissos rolável** (`.calendar-day`/`.calendar-day__events` em `calendario.css`): `.calendar-day` tem `height` fixo (108px desktop, 76px no breakpoint mobile — antes era `min-height`, o que deixava a linha inteira da grade crescer quando um dia tinha muitos compromissos, desalinhando a altura de todas as células daquela semana). Os pills não são mais filhos diretos de `.calendar-day` — `calendar-individual.js` (`render()`) os agrupa num `<div class="calendar-day__events">` (`flex:1; min-height:0; overflow-y:auto`) irmão de `.calendar-day__header`. **`.calendar-day` (o item de grid, não só o `__events` interno) também precisa de `min-height:0` + `overflow:hidden`** — sem isso, o "tamanho mínimo automático" que grid/flexbox calculam por padrão pra um item (baseado no conteúdo, ignorando `height` explícito) ainda fazia a *linha da grade* crescer pra caber todos os pills, mesmo com a célula e o `overflow-y:auto` do `__events` configurados certinho por dentro — o corte real só acontece quando o próprio item de grid para de contribuir com seu min-content pro cálculo da altura da linha (`min-height:0`/`overflow` não-visible fazem isso). Resultado: o cabeçalho (número do dia + botão de adicionar) fica sempre fixo, todas as linhas da grade têm a mesma altura sempre, e uma barra de rolagem aparece dentro da célula só quando os compromissos daquele dia não cabem nos 108px/76px disponíveis.
**Agenda completa do dia** (`#ic-day-modal`, `openDayModal()` em `calendar-individual.js`): clicar no número do dia (`.calendar-day__number`, `cursor:pointer` + destaque no hover) abre um popup com **todos** os compromissos do dia (respeitando os filtros ativos, mesmo `eventosDoDia` usado pra desenhar a célula) mais o feriado, se houver — sem o corte de altura/rolagem da célula, já que o `.day-modal-list` (`calendario.css`) tem `max-height:360px` próprio, bem maior que os 108px da grade, e os pills ali dentro voltam a ter `white-space:normal` (podem quebrar linha) em vez do `nowrap`+ellipsis da grade, então nada aparece cortado. Pra evitar duplicar a criação dos pills em dois lugares (grade e popup), `criarPillCompromisso(ev)`/`criarPillFeriado(iso, nome)` foram extraídas como funções reaproveitadas por `render()` **e** por `openDayModal()` — mesmo elemento, mesmo clique (editar/ver detalhes/ver nome do feriado), só muda o container onde entram. Clicar num item dentro do popup fecha o popup da agenda antes de abrir o modal de destino (edição/visualização/feriado), pra não empilhar dois overlays ao mesmo tempo. O botão "Novo Compromisso" do popup pré-preenche a data com o dia clicado (mesmo mecanismo do "+" de cada célula).
**Feriados no Calendário Individual** (`GET /api/feriados/?ano=AAAA`, `feriados_view` em `views.py`): usa a lib `holidays` (PyPI, `requirements.txt`) pra devolver os feriados **nacionais + estaduais do Paraná** (`holidays.Brazil(years=ano, subdiv="PR", language="pt_BR")`, categoria `public` — o default da lib, exclui pontos facultativos tipo Carnaval/Corpus Christi) do ano pedido, como `[{"data": "AAAA-MM-DD", "nome": "..."}]`. `language="pt_BR"` é passado explicitamente — sem isso, a lib pode cair pro locale do processo do servidor (que nem sempre é pt_BR, ex.: environment com `LANG`/`LANGUAGE` em inglês) em vez do `default_language` da classe `Brazil`, fazendo os nomes virem em inglês ("Independence Day" em vez de "Independência do Brasil") mesmo com o resto do portal em português. **De propósito não tem feriado municipal de Foz do Iguaçu aqui** — nenhuma lib de feriados cobre granularidade de município brasileiro (a `holidays` só tem um caso especial hardcoded pra "São Paulo Capital", nada além disso), e manter uma lista municipal certa exigiria curadoria manual + atualização por decreto da Prefeitura a cada ano; o usuário decidiu deixar de fora por enquanto, só nacional/estadual mesmo. Se algum dia precisar do municipal, a rota certa é o usuário fornecer a lista oficial (decreto da Prefeitura) pra virar uma tabela fixa no código, não tentar adivinhar/inferir datas.
No frontend (`calendar-individual.js`), `carregarFeriados(ano)` busca e cacheia por ano (`Map` em memória, só refaz a requisição ao trocar de ano); `render()` busca também o ano anterior/seguinte quando o mês exibido encosta na borda do ano (janeiro/dezembro), já que os dias "fora do mês" na grade podem pertencer a um ano diferente de `viewYear`. Cada célula de dia feriado ganha a classe `.is-feriado` (fundo tingido de vermelho, `rgba(var(--danger-rgb), 0.1)`) e um pill (`.calendar-day__holiday`, mesmo visual dos pills de compromisso — `.calendar-event`, só que com fundo `--danger` fixo, não uma cor por categoria) com o nome do feriado. Esse pill é irmão de `.calendar-day__header`, **fora** de `.calendar-day__events` — fica sempre fixo no topo da célula, não rola junto com os compromissos do dia. Truncado com `text-overflow:ellipsis` quando o nome não cabe (alguns feriados vêm com dois nomes concatenados por `;`, ex.: "Nossa Senhora do Rocio; Proclamação da República" — ver `feriados_view`); clicar no pill abre `#ic-holiday-modal` (`openHolidayModal()`, mesmo padrão dos outros modais — fecha clicando fora) mostrando a data e o nome completo sem corte. É só informativo, não bloqueia criar/editar compromisso nesse dia.
**Gerente/coordenador vê a agenda individual da equipe**: `CompromissoAgendaViewSet.get_queryset` acrescenta `Q(visibilidade="somente_eu", dono__in=usuario.liderados.all())` às regras de visibilidade — ou seja, além de "todos" e "departamento" (ver acima), quem tem gente em `Usuario.liderados` (ver seção "Liderança") também enxerga os compromissos privados (`"somente_eu"`) de cada liderado, mas sem poder editá-los (`sou_dono` continua `False` pra esses, `CompromissoAgendaViewSet.get_object` já barra escrita de quem não é dono). Não há necessidade de checar `usuario.lideranca` explicitamente na query — `liderados` só é populado através de fluxos que já exigem esse flag (ver "Liderança"), então a cláusula é inofensiva (não casa nada) pra quem não lidera ninguém.
**Lembrete com horário comercial** (`CompromissoAgenda.lembrete_antecedencia`, opcional — `""` = sem lembrete; `"1h"`/`"2h"`/`"4h"`/`"24h"`): não existe nenhum mecanismo de push/e-mail no projeto — o "lembrete" é só o momento a partir do qual o compromisso passa a aparecer no sino de notificações (`notif-bell`), que já era recalculado a cada carregamento de página (sem processo em segundo plano). `CompromissoAgenda.calcular_notificar_em()` (`models.py`) calcula esse horário contando `lembrete_antecedencia` horas **de expediente** (seg-sex, 8h-18h, `COMPROMISSO_HORARIO_COMERCIAL_INICIO`/`_FIM`) pra trás a partir de `data`+`horario` — fora do expediente não conta como antecedência "gasta", só é pulado de graça. Por isso um compromisso às 08h de segunda com lembrete de 4h não notifica às 04h (fora do expediente); o algoritmo (`_janela_comercial`/`_dia_util_anterior`, funções módulo-level) pula pro fechamento do expediente do dia útil anterior (sexta 18h) e só então desconta as 4h, resultando em sexta 14h. Sem `horario` definido no compromisso (evento de dia inteiro) ou sem `lembrete_antecedencia`, não há o que calcular e `calcular_notificar_em()` retorna `None`. O resultado é exposto só leitura via `notificar_em` no serializer (ISO datetime ou `null`); `pidBuildEventNotifications()` (`notifications.js`) só inclui um compromisso na lista do sino quando `notificar_em` não é nulo **e** já foi atingido (`now >= notificar_em`) — compromissos sem lembrete configurado simplesmente não aparecem no sino (comportamento diferente de antes da migração, quando todo compromisso futuro aparecia lá independente de qualquer configuração).
`widgets.js` mantém um registro extensível `PID_WIDGET_TYPES` (`{ "<chave>": { label, description, href, linkLabel, visibleIf? } }`) — hoje existem `"calendario-individual"` e `"links-favoritos"` (ver seção própria abaixo). `href`/`linkLabel` alimentam o link de rodapé do card ("Ver X completo →"); antes de existir um segundo tipo de widget esse link era hardcoded pra `calendario-individual.html`, então ao adicionar um tipo novo **sempre** preencher os dois, senão o rodapé de todos os widgets aponta pro lugar errado. `visibleIf(me)` é opcional — quando presente, filtra o tipo tanto do picker (`renderPicker()`) quanto da grade já adicionada (`renderWidgets()`), usado pra widgets que exponham dado de um módulo com permissão própria (ex.: `links-favoritos` só aparece pra quem tem `apps["links-ferramentas-visualizar"]` em `links-ferramentas`). Para adicionar um novo tipo de widget: registrar a entrada em `PID_WIDGET_TYPES` e adicionar um `case`/`if` em `widgetBodyFor()` que retorne o HTML do corpo do card; o picker (`#widget-picker-modal`) e a grade (`#widgets-grid`) já lidam com adicionar/remover genericamente via `/api/widgets/`.
**Reordenar e redimensionar widgets** (`WidgetUsuario.ordem`/`largura`/`altura`, por usuário): `.widgets-grid` é `display:flex; flex-wrap:wrap` (não mais CSS Grid — precisava permitir que cada `.widget-card` tivesse largura/altura próprias e livres, incompatível com colunas de grid uniformes). Reordenar é drag-and-drop nativo HTML5 igual ao de Links & Ferramentas (`dragstart`/`dragover`/`drop` em `#widgets-grid`, `PATCH /api/widgets/{tipo}/` só nos itens cujo `ordem` mudou) — a diferença é que o `draggable="true"` fica só em `.widget-card__header` (a barra de título), não no card inteiro, pra não conflitar com o handle nativo de resize (`resize: both` em `.widget-card`, ativo no canto inferior direito). Redimensionar usa esse `resize: both` do CSS (sem JS de arraste custom) — um `ResizeObserver` por card (`observeWidgetSizes()`) detecta a mudança de tamanho e salva `largura`/`altura` com debounce de 500ms; como o resize é 100% nativo do browser, não precisa nenhum cálculo manual de arraste. Como o conteúdo de um widget pode ficar maior que o espaço depois de encolhido, só `.widget-card__body` tem `overflow-y: auto` (o cabeçalho e o link de rodapé ficam fixos, só o corpo rola).
## Links & Ferramentas
"Links & Ferramentas" era uma seção do menu com uma única aplicação (a grade de cartões); virou uma seção com **duas** aplicações reais — a grade de cartões original e "Acessos Gerais" (ver seção própria abaixo) — quando essa segunda foi adicionada. Por isso o item do menu, que antes era um link direto (`<li data-section="links-ferramentas"><a href="links-ferramentas.html">`), agora é um `nav-group` expansível (mesmo padrão de "Portais"/"Auditorias") com dois `nav-subitem`: "Links & Ferramentas" (`data-app="links-ferramentas-visualizar"`, mesma URL de antes) e "Acessos Gerais" (`data-app="acessos-gerais-visualizar"`, `acessos-gerais.html`). Isso teve um efeito colateral em `favorites.js`: como o `<li data-section="links-ferramentas">` não tem mais um `<a class="nav-item">` direto (virou um `<button data-group-toggle>`), a seção como um todo deixou de ser favoritável — só os dois sub-itens são, cada um com seu próprio `app_id` derivado (`"links-ferramentas__links-ferramentas"`/`"links-ferramentas__acessos-gerais"`, ver "Favoritos" acima). Um favorito antigo com `app_id === "links-ferramentas"` (de antes dessa mudança) para de casar — mesma categoria de caveat já documentada em "Favoritos": mudar a forma como um item aparece no menu muda o `app_id` derivado.
`LinkFerramenta` é uma lista **global/compartilhada** (sem FK pra `Usuario`, ao contrário de `Favorito`/`WidgetUsuario`/`NotificacaoDispensada`) — todo usuário com `apps["links-ferramentas-visualizar"]=True` em `permissoes["links-ferramentas"]` vê os mesmos cartões via `GET /api/links-ferramentas/` (ver "Padrão visualizar/editar" acima).
`links-ferramentas.js` também gateia o **conteúdo da própria página** por `apps["links-ferramentas-visualizar"]` (`#lf-no-access`/`#lf-content` em `links-ferramentas.html`, mesmo padrão do `.no-access` de `portal.html`) — isso existe porque o sidebar (`data-section="links-ferramentas"` em `access.js`) só esconde o `nav-group` inteiro com base no `enabled` do módulo (e cada sub-item individualmente com base no seu `-visualizar`), então alguém sem `apps["links-ferramentas-visualizar"]` mas que navegue direto pra URL (ou tenha `enabled=true` sem essa flag, uma combinação tecnicamente possível já que são independentes) via GET no backend recebia 403 e via a tela renderizada com "Nenhum link cadastrado ainda." em vez de uma mensagem de acesso negado. Ao adicionar uma aplicação nova no padrão visualizar/editar, replicar esse gate (como `acessos-gerais.js` já faz) — não basta confiar em `access.js` escondendo o link do menu.
Só quem tem `apps["links-ferramentas-editar"]=True` (`me.permissoes_efetivas["links-ferramentas"].apps["links-ferramentas-editar"]`, já unido no servidor) vê em `links-ferramentas.html` os controles de administração: botão "Adicionar Link" no topo e, em cada cartão, setas de mover para cima/baixo + X de remover (`links-ferramentas.js`, gated no frontend por essa flag, e reforçado no servidor por `PermissaoApp("links-ferramentas", "links-ferramentas-editar")`/`PermissaoApp("links-ferramentas", "links-ferramentas-visualizar")` conforme o método HTTP).
- **Ordenação**: campo `ordem` (inteiro, sem `unique`) em `LinkFerramenta`, `Meta.ordering = ["ordem", "id"]`. Não existe endpoint de reorder em lote no backend — o cliente é quem decide quais `PATCH`es disparar. Duas formas de reordenar na UI, ambas em `links-ferramentas.js`: as setas (`swapOrdem()`) trocam o `ordem` de dois itens adjacentes com duas chamadas `PATCH`; arrastar um cartão (drag-and-drop nativo HTML5, `.lf-card--draggable`/`dragstart`/`dragover`/`drop` no `#lf-grid`) recalcula a lista inteira em memória e envia um `PATCH` só para os itens cujo `ordem` (índice na nova ordem) realmente mudou — como não há `unique` em `ordem`, não tem problema disparar essas chamadas em paralelo (`Promise.all`) mesmo que dois itens fiquem com o mesmo valor por um instante. Ao criar um link novo, o servidor sempre calcula `ordem = max(ordem atual) + 1` em `LinkFerramentaViewSet.perform_create` — qualquer `ordem` enviada pelo cliente no POST é ignorada.
- **Ícone**: `icone` é um `ImageField` opcional (upload real, não URL) — exige Pillow (`requirements.txt`) e `MEDIA_URL`/`MEDIA_ROOT` (`settings.py`, servido em `DEBUG` por `config/urls.py`). Sem ícone, o cartão cai num SVG de fallback (`PID_LINK_DEFAULT_ICON` em `links-ferramentas.js`, mesmo glifo do item "Links & Ferramentas" no menu). O upload viaja como `multipart/form-data` (`FormData`), não JSON — ver a nota sobre `pidApiRequest` acima. Limite de tamanho: **2MB**, checado em dois lugares — `validar_tamanho_icone_link` (validator do campo `icone` em `models.py`, é a checagem que vale de verdade, roda via `LinkFerramentaSerializer.is_valid()`) e uma checagem espelhada em `links-ferramentas.js` (`PID_LINK_ICON_MAX_BYTES`, no `change` do input e de novo antes do POST/PATCH) só para dar feedback sem esperar a resposta do servidor. Ao mudar o limite, atualizar os dois lados (e gerar migração — `validators` no campo entra no `deconstruct()`).
- Clicar num cartão sempre abre a URL numa aba nova (`target="_blank"`) — são links externos por definição, não faz sentido navegar embutido no portal.
- Editar um cartão existente (nome, URL e ícone) usa o mesmo modal de "Adicionar Link" (`#lf-add-modal`), reaproveitado em modo edição — o botão de lápis em cada cartão (visível só com `apps["links-ferramentas-editar"]`, ao lado das setas de mover) chama `openModal(link)` pré-preenchendo os campos; salvar despacha `PATCH /api/links-ferramentas/{id}/` (`pidUpdateLink`, multipart igual ao POST) em vez de criar um novo. O campo de ícone fica sempre vazio ao abrir em modo edição (input `type="file"` não aceita valor pré-preenchido por segurança do browser) — não enviar o campo `icone` no PATCH mantém o ícone atual; só enviar substitui.
- **Favoritos por link** (`LinkFerramentaFavorito`, model dedicado — não confundir com `Favorito`, que marca aplicações inteiras do menu): estrela em cada cartão (`.lf-card__favorite`, visível pra qualquer um com `apps["links-ferramentas-visualizar"]`, independente de editar) via `POST`/`DELETE /api/links-ferramentas-favoritos/{link_id}/` (natural key é o `id` do link, igual ao padrão `app_id`/`notif_id` de `Favorito`/`NotificacaoDispensada`). Só afeta a **ordem de exibição dentro da própria tela** — `links-ferramentas.js` busca `links` e favoritos em paralelo e reordena em memória (`sortFavoritesFirst()`) pra mostrar favoritos primeiro, preservando o `ordem` relativo dentro de cada grupo; o `ordem` compartilhado do `LinkFerramenta` nunca é tocado por favoritar/desfavoritar. Simplificação deliberada: as setas de mover e o drag-and-drop operam sobre esse mesmo array já reordenado (`links`), então se um usuário com `apps["links-ferramentas-editar"]` também tiver favoritos marcados, arrastar um cartão persiste a ordem "favoritos primeiro" que ele está vendo como o novo `ordem` compartilhado — aceitável pro tamanho do app, mas vale lembrar se o comportamento parecer estranho.
- **Widget "Links Favoritos"** (`links-favoritos` em `PID_WIDGET_TYPES`, `static/js/widgets.js`): lista em `portal.html` só os links favoritados, cada linha com ícone pequeno (`.widget-links-list__icon`, fallback `PID_WIDGET_LINK_DEFAULT_ICON` — cópia local do glifo de `PID_LINK_DEFAULT_ICON`, já que `widgets.css` não carrega `links-ferramentas.css`) + nome, a linha inteira é um `<a target="_blank">` pro mesmo destino do cartão original. Depende de `pidFetchLinks`/`pidFetchLinkFavoritos`, então `links-ferramentas.js` foi incluído em `portal.html` só por causa dessas funções de dados — seu handler de `DOMContentLoaded` retorna cedo lá (`if (!grid) return`, não existe `#lf-grid` em `portal.html`), mesmo padrão de guarda de `profiles.js`/`widgets.js`.
## Acessos Gerais
Segunda aplicação da seção "Links & Ferramentas" (ver acima) — um cadastro de acessos/logins compartilhados (ex.: "login geral de um site"), organizado em **seções e linhas** (inspirado numa tela do Asana que o usuário mostrou como referência): cada seção agrupa várias linhas, e clicar numa linha abre um popup com os detalhes daquele acesso. Dois models novos, sem relação com `LinkFerramenta`:
- `AcessoGeralSecao` (`nome`, `ordem`, `perfis_restritos` M2M pra `PerfilAcesso`, ver abaixo) — só a categoria (ex.: "Banco de Dados/API"). Lista compartilhada, sem FK pra `Usuario`.
- `AcessoGeral` (`secao` FK, `nome`, `url`, `usuario`, `senha`, `observacoes`, `ordem`) — a linha em si. `senha` é um `CharField` em texto puro (não há criptografia/hash — é um cadastro de referência entre a própria equipe, não um cofre de senhas robusto; se isso precisar mudar no futuro, confirmar com o usuário antes, já que envolve infraestrutura de chave/criptografia nova). `observacoes` guarda **HTML sanitizado** (ver "Observações ricas" abaixo), com um `validators=[validar_tamanho_observacoes_acesso]` (`models.py`) limitando a 2.000.000 caracteres — generoso o bastante pra várias imagens embutidas, mas evita um `TextField` sem limite nenhum crescer sem controle.
**Permissão**: mesmo padrão visualizar/editar de Links & Ferramentas, com chaves próprias (`acessos-gerais-visualizar`/`acessos-gerais-editar`, ver "Padrão visualizar/editar" acima) — `AcessoGeralSecaoViewSet`/`AcessoGeralViewSet` (`views.py`) instanciam `PermissaoApp("links-ferramentas", app_key)` com a chave certa por método HTTP. `acessos-gerais.js` gateia o conteúdo da própria página (`#ag-no-access`/`#ag-content`) por `acessos-gerais-visualizar`, mesmo raciocínio do gate de `links-ferramentas.js`.
**Restrição de seção por perfil** (`AcessoGeralSecao.perfis_restritos`): além da permissão de módulo, cada seção pode opcionalmente ser restrita a um subconjunto de `PerfilAcesso` — `perfis_restritos` vazio (padrão) = visível a qualquer um com `acessos-gerais-visualizar`; não vazio = só quem também tiver um desses perfis vinculado. Isso é uma restrição de **dado**, independente da árvore de permissões (não precisa mexer em Perfis de Acesso pra configurar) — é escolhida direto no modal "Adicionar Seção"/"Renomear Seção" (`#ag-secao-form-perfis`, um `.checklist-box` com todos os perfis cadastrados, populado via `pidFetchPerfis()`). O filtro é aplicado em dois lugares no backend, ambos em `views.py`:
- `AcessoGeralSecaoViewSet.get_queryset()` — só devolve seções sem restrição ou com interseção entre `perfis_restritos` e os perfis do usuário logado; `AcessoGeralViewSet.get_queryset()` aplica o mesmo filtro via `secao__perfis_restritos`, pra uma linha nunca vazar de uma seção que o usuário não veria.
- `AcessoGeralSerializer.__init__` também restringe o próprio campo `secao` (o `PrimaryKeyRelatedField` que valida o POST/PATCH) à mesma queryset filtrada — sem isso, alguém com `acessos-gerais-editar` mas sem o perfil exigido conseguiria criar uma linha dentro de uma seção restrita só sabendo o id dela, mesmo sem enxergá-la em nenhuma listagem.
Não há exceção pra `gerencia_permissoes`/perfil de acesso total — mesmo "Integração e Inovação" (código 8) fica de fora de uma seção restrita a outro perfil que não o seu, exatamente como qualquer outro perfil (é uma lista de permissão explícita, não um nível hierárquico).
**Ordenação por seção**: `AcessoGeral.ordem` é **escopada por `secao`** (ao contrário de `LinkFerramenta.ordem`, que é global) — `AcessoGeralViewSet.perform_create` calcula `max(ordem)` só entre as linhas da mesma seção. Reordenar (drag-and-drop nativo HTML5, mesma mecânica de `links-ferramentas.js` — `dragstart`/`dragover`/`drop` em `#ag-sections`, delegado num container que tem todas as seções) só é permitido **dentro de uma seção**: `dragover` ignora o alvo se `draggedAcesso.secao !== targetAcesso.secao`, então uma linha nunca muda de seção arrastando. As setas de mover para cima/baixo (`swapOrdem()`) seguem a mesma regra, já que operam sobre `acessosDaSecao(secao.id)`, nunca a lista inteira. Seções em si não têm drag-and-drop — só criar/renomear/excluir; a ordem entre seções é a de criação (`ordem` incrementado pelo servidor, sem UI de reordenar).
**Observações ricas (texto + imagens embutidas)**: o campo "Observações" do modal de acesso (`#ag-form-observacoes`) é um `<div contenteditable>`, não um `<textarea>` — permite formatar texto livremente e incluir imagens **sem nenhum botão dedicado**: colar (`Ctrl+V`, evento `paste`, lido de `event.clipboardData.items`) ou arrastar um arquivo de imagem pra dentro do campo (evento `drop`, com `dragover` chamando `preventDefault()` pra permitir o drop) — as duas vias caem na mesma função `insertImageFile()` em `acessos-gerais.js`. A imagem (até **2MB**, `PID_AG_IMAGE_MAX_BYTES`, checado antes de inserir) vira uma data URI via `FileReader.readAsDataURL` e é inserida com `document.execCommand("insertImage", ...)` — sem upload de arquivo separado, fica embutida no próprio HTML salvo em `observacoes`. No caminho de `paste` o cursor já está na posição certa (o navegador só troca o clipboard, não move o foco); no de `drop`, `placeCaretAtPoint()` usa `document.caretRangeFromPoint`/`caretPositionFromPoint` (conforme suporte do browser) pra posicionar o cursor exatamente onde o arquivo foi solto antes de inserir.
- **Sanitização (`nh3`)**: como esse HTML é gerado por quem tem `acessos-gerais-editar` mas renderizado via `innerHTML` pra qualquer um com `acessos-gerais-visualizar`, ele passa por um allowlist estrito no backend antes de salvar — `AcessoGeralSerializer.validate_observacoes()` roda `nh3.clean()` permitindo apenas tags de texto básicas + `<img>` (`ACESSO_GERAL_OBSERVACOES_ALLOWED_TAGS`/`_ALLOWED_ATTRS`/`_ALLOWED_SCHEMES` no topo de `serializers.py`) — **sem `<a>`/`<script>`/atributos de evento** (`onerror` etc. são descartados por não estarem na allowlist de atributos). `url_schemes` inclui `"data"` de propósito, já que as imagens embutidas são `data:image/...;base64,...`, não URLs externas. Isso significa que o campo é reprocessado no servidor mesmo que o cliente já não deixe inserir nada além de texto/imagem pela UI — defesa em profundidade contra alguém montando o payload na mão. `nh3` é o binding Python da lib Rust "ammonia" (`requirements.txt`) — foi escolhido no lugar do `bleach` (usado numa primeira versão desta funcionalidade) porque o `bleach` está oficialmente sem manutenção desde 2023 e parou de receber releases (inclusive de segurança) em 2026; `nh3` tem API quase idêntica (`clean(html, tags=set[...], attributes=dict[...], url_schemes=set[...])`, allowlist do mesmo jeito) e é o substituto recomendado pelos próprios mantenedores do bleach.
- Ao carregar um acesso existente pra editar, `formObservacoes.innerHTML = acesso.observacoes` repopula o editor com o HTML já sanitizado (imagens inclusas); salvar lê `formObservacoes.innerHTML` (função `observacoesValue()`, que retorna string vazia se não houver nem texto nem `<img>`, evitando salvar lixo tipo um `<br>` solto de um editor "vazio").
**Popup de detalhes** (`#ag-view-modal`, `acessos-gerais.js`): mostra nome, URL (link clicável), usuário, senha e observações — cada campo (`.ag-view-field`) só aparece se tiver valor (`hidden` quando vazio). A senha começa mascarada (`"••••••••"`, com o valor real guardado em `viewSenha.dataset.value`) e um botão de olho alterna pra o valor real — a máscara é feita trocando o próprio `textContent`, não com CSS (`-webkit-text-security` não é suportado em todos os browsers e deixaria a senha real exposta no DOM seletável mesmo "mascarada" visualmente nesses casos). As observações são renderizadas via `innerHTML` (não `textContent`, ao contrário dos outros campos) já que podem conter as imagens embutidas — seguro porque o HTML já veio sanitizado do backend; o container é uma `<div class="ag-view-observacoes">` (não `<p>`, que não pode conter `<img>`/`<div>` sem gerar HTML inválido). Com `acessos-gerais-editar`, o popup também mostra "Editar"/"Excluir"; "Editar" fecha o popup e abre o mesmo modal de formulário (`#ag-form-modal`) usado por "Adicionar Acesso", pré-preenchido.
Nenhuma tela recalcula união de departamentos/liderança aqui — é uma aplicação isolada, sem relação com `Usuario` além da permissão de quem pode ver/editar (e, agora, do `perfis_restritos` por seção).
## Ramais
O diretório de `ramais.html` é **automático**: `RamalViewSet.list()` (não o `RamalSerializer` — esse serializer só cobre as linhas avulsas via CRUD normal) mescla, a cada `GET /api/ramais/`, duas fontes numa lista só, ordenada por nome:
1. Todo `Usuario` ativo — a linha é montada direto do cadastro (`nome`, `Usuario.departamentos` juntados por vírgula, `Usuario.ramal`); se o colaborador ainda não tem ramal preenchido, `numero_exibicao` vem vazio e o frontend mostra um badge "Pendente" em vez de reclamar de campo faltando.
2. As linhas avulsas de `Ramal` (sem `Usuario` por trás — telefone de sala, recepção etc.), cadastradas pelo modal "Adicionar Ramal".
Cada item da lista mesclada tem um `id` sintético (`"usuario-<id>"` ou `"avulso-<id>"`) e um campo `tipo` (`"usuario"`/`"avulso"`) que o frontend usa pra decidir qual endpoint chamar ao editar/excluir — não existe mais um model unificando os dois casos com uma FK opcional (essa foi a primeira versão da tela; revertida a pedido do usuário pra eliminar o passo manual de "adicionar" alguém que já tem cadastro).
Segue o mesmo padrão visualizar/editar de Links & Ferramentas (ver acima): leitura exige `apps.visualizar` (liberado a todo perfil, já que `ramais` está em `BASE_KEYS`), escrita exige `apps.editar` — por ora só `True` pra "Integração e Inovação" no `seed_portal.py`, exatamente como pedido; liberar outro perfil não pede código novo, só marcar o app na árvore de Perfis de Acesso.
- **Editar o ramal de um colaborador de verdade**: não existe "criar" — a linha já aparece sozinha. O lápis na linha abre o mesmo modal de Ramal, mas com Nome/Departamento desabilitados (só leitura do cadastro) e só o campo Ramal editável; salvar chama `PATCH /api/ramais/usuarios/{usuario_id}/` (`RamalViewSet.atualizar_ramal_usuario`), que grava direto em `Usuario.ramal` — é assim que a tela demonstra a alteração refletindo no cadastro do usuário.
- **Linha avulsa**: "Adicionar Ramal" sempre cria uma linha avulsa (`POST /api/ramais/`, `nome`/`departamento`/`numero` livres — só `nome` é obrigatório, o resto pode ficar pendente igual a um colaborador de verdade). Editar/excluir uma linha avulsa usa `PATCH`/`DELETE /api/ramais/{avulso_id}/` normalmente; excluir só existe pra esse tipo (não dá pra "excluir" um colaborador daqui — isso é na tela de Usuários).
- **Lista de usuários do modal de Ausência**: `RamalViewSet.usuarios_disponiveis` (`GET /api/ramais/usuarios/`) devolve só `id`/`nome` de usuários ativos, pra alimentar o `<select>` "Lista de Usuários" do modal "Criar Ausência" (o único modal que ainda precisa escolher uma pessoa numa lista — o modal de Ramal não precisa mais, já que a linha do colaborador já existe). Não reaproveita `/api/usuarios/` de propósito — aquele endpoint é restrito a `gerencia_permissoes`, e a permissão de Ramais é deliberadamente desacoplada disso (hoje dá na mesma pessoa, mas não presume que sempre será assim).
- **Ausência** (`RamalAusencia`): um registro por período criado pelo modal "Criar Ausência"; "ausente agora" nunca é armazenado — `RamalAusencia.esta_ativa()` compara a hora atual (`timezone.localtime()`) contra `[data_inicio+hora_inicio, data_fim+hora_volta]` (hora ausente = considera o dia inteiro) toda vez que `RamalViewSet.list()` monta a resposta. Clicar no badge "(AUSENTE)" de uma linha (`data-ram-ver-ausencia`, qualquer um com `apps.visualizar` pode abrir) faz `GET /api/ramais-ausencias/{id}/` e abre o modal "Visualizar Ausência" — campos desabilitados (`<input type="date"/"time">` mostra a data/hora formatada mesmo `disabled`, sem precisar formatar manualmente), com "Editar Ausência"/"Deletar Ausência" visíveis só pra quem tem `apps.editar`. "Editar" habilita os mesmos campos in-place (sem modal novo) e troca os botões por "Cancelar"/"Salvar Ausência" (`PATCH /api/ramais-ausencias/{id}/`); "Deletar" remove o registro (`DELETE`) — não existe mais um botão de "encerrar antes do previsto" separado (a rodada anterior tinha isso via `encerrada_manualmente`; substituído por editar/excluir de verdade, mais simples e é o que foi pedido). O campo `encerrada_manualmente` continua no model (histórico/uso futuro via admin), só não tem mais UI própria.
- **Aniversariante**: comparação de `Usuario.data_aniversario` (mês/dia) com `timezone.localdate()`, feita no mesmo `list()` — mesmo campo que já existia no cadastro de Usuários, sem nada novo ali.
- **Selos de ausente/aniversariante**: `.ram-badge--ausente`/`.ram-badge--aniversario` (`ramais.css`) são selos (pill) com cor de texto/fundo ajustada por tema via `:root[data-theme="light"] .ram-badge--*` — não reaproveitam `--danger`/`--gold` crus porque esses tokens não foram pensados pra texto pequeno sobre um selo (contraste insuficiente). A linha inteira também é tingida (`.ram-row--ausente`/`.ram-row--aniversario` td, aplicado via classe no `<tr>` em `ramais.js`) com a mesma cor do selo, também ajustada por tema — pedido explícito do usuário pra facilitar notar a linha antes mesmo de ler o selo (a versão anterior sem tingimento de linha foi revertida).
- **Férias**: a aba existe (navegação por abas, ver abaixo) mas está **vazia de propósito** — o conteúdo foi adiado pra uma rodada futura; a limitação original ("depende de integração futura com outro banco") continua valendo, só a decisão de já reservar o espaço na navegação é nova.
- **Novo Chamado**: botão que abre um modal com um `<iframe>` apontando para a ferramenta externa de chamados (`https://depaula-tvcorporativa.lovable.app/chamar?token=...`) — decisão explícita de ficar embutido na própria tela em vez de nova aba (diferente do padrão dos demais links externos do portal). O `src` do iframe só é setado na abertura do modal e volta pra `about:blank` ao fechar, pra não deixar a ferramenta carregada em segundo plano.
- **Sem reordenação**: ao contrário de Links & Ferramentas/Widgets, a listagem é sempre alfabética (`sort()` em `list()`), sem `ordem`/drag-and-drop.
- Usuário inativo (`is_active=False`) não aparece mais no diretório (o `list()` filtra `Usuario.objects.filter(is_active=True)`) — diferença deliberada da primeira versão, que ainda mostrava inativos se tivessem uma linha vinculada.
### Navegação por abas em `ramais.html` (subtelas)
`ramais.html` deixou de ser uma tela única — é uma seção com 5 subtelas, navegáveis por abas logo abaixo do cabeçalho: **Ramais** (diretório descrito acima, ativa por padrão), **Responsável no Tareffa** (placeholder vazio), **Telefones Externos**, **Férias** (placeholder vazio) e **Funções de Telefonia**. As abas reaproveitam o CSS genérico `.pa-tabs`/`.pa-tab`/`.pa-tab-panel` (`perfis-acesso.css`, já carregado nesta página desde antes — mesmo padrão usado nas abas Permissões/Usuários de `perfis-acesso.html`), mas com atributos próprios (`data-ram-tab`/`data-ram-tab-panel`) e uma implementação independente em `ramais.js` (`activeRamTab`/`renderRamTabs()`), pra não colidir com `profiles.js`. Os botões "Adicionar Ramal"/"Novo Chamado"/"Criar Ausência" continuam só dentro do painel "Ramais" — cada subtela tem suas próprias ações.
**Permissão — uma dupla visualizar/(editar) por subtela**: cada uma das 5 abas tem sua própria permissão de visualização, e as 3 com conteúdo administrável (Ramais, Telefones Externos, Funções de Telefonia) também têm sua própria permissão de edição — não é mais um único par genérico `ramais.apps.visualizar`/`ramais.apps.editar` cobrindo tudo (esse desenho, usado na primeira versão da navegação por abas, foi revisto no mesmo dia a pedido do usuário: "deve haver permissão de visualização para cada um dos itens e edição para as de ramais, telefone externos e funções de telefonia"). Em `catalogo.MODULE_APPS["ramais"]`, isso é modelado como **5 subgrupos** (mesmo formato `{"key", "label", "tools": [...]}` já usado em Auditorias — reaproveita 100% a árvore de permissões genérica de `profiles.js`, sem UI nova):
```python
"ramais": [
{"key": "ramais-diretorio", "label": "Ramais", "tools": [
{"key": "ramais-visualizar", "label": "Visualizar"},
{"key": "ramais-editar", "label": "Editar (...)"},
]},
{"key": "responsavel-tareffa", "label": "Responsável no Tareffa", "tools": [
{"key": "responsavel-tareffa-visualizar", "label": "Visualizar"},
]},
{"key": "telefones-externos", "label": "Telefones Externos", "tools": [...]},
{"key": "ferias", "label": "Férias", "tools": [{"key": "ferias-visualizar", ...}]},
{"key": "funcoes-telefonia", "label": "Funções de Telefonia", "tools": [...]},
],
```
Cada `ModelViewSet` (`RamalViewSet`/`RamalAusenciaViewSet`, `TelefoneExternoViewSet`, `FuncaoTelefoniaViewSet`) instancia `PermissaoApp("ramais", app_key)` com a chave da própria subtela (ex.: `"telefones-externos-visualizar"`/`"telefones-externos-editar"`) — `RamalAusenciaViewSet` usa as mesmas chaves `ramais-visualizar`/`ramais-editar` do diretório de Ramais, já que ausência é parte dessa subtela, não uma quinta. No frontend, `ramais.js` calcula um `canView`/`canManage` por subtela a partir de `me.permissoes_efetivas.ramais.apps[chave]`, esconde (`hidden`) o botão de cada aba cujo `visualizar` for falso, e escolhe a primeira aba visível como ativa por padrão (em vez de sempre abrir em "Ramais", que pode estar oculta pra esse perfil). `ramais-lookup.js` (modal de consulta rápida no topbar) usa especificamente `ramais-visualizar`, já que só mostra o diretório de Ramais, não as outras subtelas.
**Cuidado com `seed_portal.py`** (mesmo princípio da nota geral em "Padrão visualizar/editar" acima): como `ramais` está em `BASE_KEYS`, `permissions_from_keys()` habilitaria os 8 apps (visualizar de todas as 5 + editar das 3) de uma vez — sem o override, todo perfil nasceria podendo editar. Por isso `seed_portal.py` força `ramais-editar`/`telefones-externos-editar`/`funcoes-telefonia-editar` para `False` explicitamente em todo perfil que não seja "Integração e Inovação", depois de montar o dict — os `*-visualizar` ficam `True` pra todo mundo de propósito ("os demais terão acesso para visualizar todas"). Qualquer mudança de nome/adição de subtela nesse padrão precisa replicar esse mesmo cuidado.
Um perfil só-visualizar vê as 5 abas e as tabelas, mas nunca os botões de Adicionar/editar/excluir em nenhuma delas; um perfil sem `visualizar` numa subtela específica não vê nem a aba dela.
**Telefones Externos** (`TelefoneExterno`, model dedicado sem FK — contatos de fornecedores/terceiros, não de `Usuario`): CRUD simples via `/api/telefones-externos/`, só `nome` obrigatório (`ramal`/`telefone`/`observacoes` opcionais, mesmo padrão de `Ramal` avulso). Dois filtros de busca (`ram-tel-search-nome`/`ram-tel-search-obs`, client-side sobre o array já carregado) — por nome e por observações, ao mesmo tempo, sem OR/AND configurável. A tabela começa vazia (nenhum seed) — o usuário cadastra pela própria tela.
**Funções de Telefonia** (`FuncaoTelefonia`) — comandos padrão da central telefônica (ex.: `*01 + Código de Agente` → LogOn). CRUD via `/api/funcoes-telefonia/`, só `comando` obrigatório. `Meta.ordering = ["comando"]` reproduz sozinho a ordem esperada (`*0, *01, ..., *5, *503, *8`) porque os códigos já nascem em ordem lexicográfica como string — não precisou de um campo `ordem` manual nem de endpoint de reorder, ao contrário de `LinkFerramenta`/`Favorito`/`WidgetUsuario`. Ao contrário de Telefones Externos, esta tabela **é seedada**: `seed_portal.py` popula as 13 linhas padrão (`FUNCOES_TELEFONIA_SEED`, `update_or_create` por `comando`) porque é documentação genérica de central telefônica, não dado específico da empresa — reexecutar `seed_portal` é seguro/idempotente, não duplica nem apaga linhas editadas manualmente (só atualiza `funcao`/`resumo` de um `comando` que já exista).
Nenhuma das duas subtelas tem endpoint de reorder — só criar/editar/excluir, mesmo escopo pedido.
### Modal de consulta rápida ("Ramais")
O botão "Ramais" do topbar (`#ramais-btn`, presente em `portal.html`/`links-ferramentas.html`/`calendario-individual.html` — as únicas 3 páginas que têm esse atalho; texto era "Acessar Ramais", encurtado depois) **não navega** para `ramais.html`; abre um modal somente-leitura (`ramais-lookup.js`/`ramais-lookup.css`) com a mesma listagem mesclada de `GET /api/ramais/`, inspirado numa tela do portal antigo (estilo DataTables: "Mostrar N registros", busca, colunas ordenáveis, paginação). Diferenças pro comportamento antigo do botão:
- Gate de acesso: some (`hidden`) se `permissoes_efetivas.ramais.apps["ramais-visualizar"]` for falso — mesmo padrão de qualquer UI gated por permissão no app.
- Busca é **uma só caixa** (não uma por coluna) que filtra por nome, departamento ou ramal ao mesmo tempo — mais simples que a paginação em duas caixas da própria `ramais.html`.
- **Botões de filtro por departamento** (`.ram-lookup-depto-filters`, acima da tabela): "Todos" + um botão por `Departamento` cadastrado, buscados de `GET /api/departamentos-resumo/` na primeira abertura (endpoint dedicado, `IsAuthenticated` + checagem manual de `permissao_app("ramais", "ramais-visualizar")` — não reaproveita `/api/departamentos/`, que exige `gerencia_permissoes` e bloquearia a maioria dos usuários que só têm acesso ao próprio Ramais). Clicar num botão filtra a listagem pra quem tem aquele departamento entre os seus (`departamento_exibicao.split(",")`, comparação exata após `trim` — não substring, pra não casar um departamento que seja prefixo de outro) e combina com a busca por texto (as duas condições precisam bater). Como os botões são gerados a partir da lista de departamentos vinda da API a cada abertura do modal, cadastrar um departamento novo em Usuários já basta pra ele aparecer aqui — não precisa mexer no frontend.
- Ordenação por coluna (clicar no cabeçalho alterna asc/desc) e paginação (`10`/`25`/`50`/`100` por página) são só client-side, sobre o array já carregado — sem endpoint novo, sem parâmetro de query; os `/api/ramais/`/`/api/departamentos-resumo/` são buscados uma única vez por abertura de página (cacheados em memória enquanto a página não recarrega) e refiltrados/reordenados em JS a cada tecla/clique.
- Botão "Ir para Controle de Ramais" no rodapé é o link de verdade pra `ramais.html` (tela completa, com edição) — o modal em si não tem nenhum controle de escrita, é só consulta.
## Solicitações
Os 6 tópicos do menu "Solicitações" (`catalogo.MODULE_APPS["solicitacoes"]`) não são telas próprias — cada um (exceto "Ordem de Serviço", que ainda não tem link definido e continua com `href="#"`, mesmo padrão de qualquer aplicação-placeholder do portal) é só um link externo (hoje, um formulário do Asana) aberto em **nova aba** (`target="_blank" rel="noopener noreferrer"`), igual ao padrão já usado nos cartões de Links & Ferramentas.
**Por que não embutido em iframe**: a primeira versão desta seção tentava centralizar os 5 links num popup com `<iframe>` (numa página dedicada `solicitacoes.html`), inspirado no "Novo Chamado" de Ramais. Revertido no mesmo dia: o Asana bloqueia ser carregado em iframe de outro domínio via `X-Frame-Options`/`Content-Security-Policy: frame-ancestors` (proteção padrão contra clickjacking), então o navegador recusa a conexão (`net::ERR_BLOCKED_BY_RESPONSE`/"A conexão com form.asana.com foi recusada"). Isso não tem workaround no frontend — não confundir com o iframe de "Novo Chamado" em Ramais ou o de "Calendário De Paula" (ver nota logo após a tabela de páginas, em "Páginas" acima), que funcionam porque aquela outra ferramenta (`depaula-tvcorporativa.lovable.app`) não bloqueia embed. Não reintroduzir esse padrão de iframe pra Solicitações sem confirmar com o usuário que o destino realmente permite ser embutido.
## Simulação de Custo de Contratação (Geradoc)
Ferramenta que substitui a planilha manual de custo de contratação (`projects/planilha de custo/*.xlsx`) por um formulário no Portal — calcula o custo de contratar um Empregado CLT e devolve um PDF pronto pra enviar ao cliente. Permissão de **toggle único** (`apps["simulacao-custo-contratacao"]` em `permissoes["geradoc"]`, sem par visualizar/editar), checada manualmente (`request.user.permissao_app("geradoc", "simulacao-custo-contratacao")`) nas duas views (não são `ModelViewSet` — são funções simples, `POST /api/simulacao-custo-contratacao/gerar/` e `GET`/`PATCH /api/parametros-fiscais-custo-contratacao/`).
- **Escopo v1: só Empregado CLT.** O pedido original mencionava 5 modalidades (Simples Nacional, Regime Normal, Pró-labore, Empregado, Empregado Doméstico), mas só havia planilha de referência validada pra Empregado CLT — as outras 4 ficam para quando houver uma fonte de regras equivalente confirmada pelo contador; não implementar "seguindo o mesmo padrão" por conta própria.
- **Sem persistência**: `POST /api/simulacao-custo-contratacao/gerar/` é um cálculo pontual — recebe os dados do formulário, calcula (`custo_contratacao.calculo.calcula_custo_empregado`) e devolve o PDF direto (`HttpResponse` binário, `Content-Disposition: inline`), nada é salvo no banco. Diferente do padrão "com histórico" de `ImportacaoPlanoSaude`/`IndicadorApuracao`.
- **Tabelas de INSS/IRRF editáveis pelo banco**: `ParametroFiscalCustoContratacao` (`models.py`) é um singleton (`atual()`, sempre `pk=1`, criado sob demanda via `get_or_create`) com `faixas_inss`/`faixas_irrf` em `JSONField` (lista de `{limite_superior, aliquota, deduzir}`) + escalares (teto de desconto de INSS, alíquota/dedução do IRRF acima da última faixa, desconto simplificado do IRRF, dedução por dependente, e os 3 parâmetros da redução da Lei 15.270/2025 — coeficientes A/B e limite de rendimento bruto), editáveis pelo painel colapsável da própria tela (`GET`/`PATCH /api/parametros-fiscais-custo-contratacao/`, mesma permissão de quem usa a simulação). `custo_contratacao/tabelas.py` continua existindo só como **seed/default** da primeira criação da linha (`_faixas_inss_padrao`/`_faixas_irrf_padrao` em `models.py`) — `calculo.py` nunca lê `tabelas.py` direto, sempre recebe um `ParametrosFiscais` (dataclass pura, sem ORM) montado por `ParametroFiscalCustoContratacao.para_calculo()`.
- **Redução de IRRF da Lei nº 15.270/2025** (art. 3º-A da Lei 9.250/1995, vigente desde jan/2026): isenção total até R$5.000 de rendimento bruto mensal, redução decrescente até zerar em R$7.350 — `redução = max(0, coeficiente_a − coeficiente_b × rendimento bruto)`, aplicada por cima do imposto já calculado pela tabela progressiva tradicional (que a lei não alterou), nunca deixando o imposto final negativo.
- **Correção deliberada em relação à planilha original**: a planilha nunca somava a dedução por dependente (R$189,59/dependente) à base do IRRF quando usava o desconto real de INSS — só quando usava o desconto simplificado (que por lei substitui os dois). Confirmado como gap com o usuário e corrigido: ao usar o desconto real de INSS, a dedução por dependente também é subtraída agora (`custo_contratacao/calculo.py`).
- **PDF via `reportlab`** (pure-Python, sem dependência nativa problemática no Windows) — cabeçalho é um banner marrom escuro com `logo-branco.png` + "De Paula Contadores", nas cores reais da marca (dourado `#D3AF4D`, marrom `#4A3C28`, amostradas do próprio `logo.png`), não o roxo do tema de interface do Portal.
- Localização no menu (dentro de **Geradoc**, ao lado de "Gerar Contrato"/"Gerar Procuração") foi escolha explícita do usuário, não Utilitários.
## Indicador de Desempenho (Geradoc)
Segunda ferramenta de Geradoc — substitui a apuração manual do indicador de desempenho do Fiscontábil (departamentos Contábil/Fiscal), antes feita numa planilha (`FISCO CONTABIL *.ods`, em `projects/Indicadores/`) com fórmulas quebradas por edições manuais acumuladas. Mesmo padrão de permissão de **toggle único** de Simulação de Custo de Contratação (`apps["indicador-desempenho"]` em `permissoes["geradoc"]`, checado por `PermissaoApp("geradoc", "indicador-desempenho")` em todos os `ModelViewSet` relacionados). Pacote de negócio em `portal_api/indicadores/` (sem ORM): `tipos.py`, `leiaute.py`, `pipeline.py`, `entregas.py`, `calculo.py`, `recibo.py`, `departamentos.py`.
- **Escopo v1: só o Fiscontábil**, papéis Balancete/Liberação Fiscal/Conciliação Financeira. Outros departamentos ficam pra rodada futura — **exceto pela estrutura de cadastro em si** (ver "Departamento organizacional" abaixo), que já suporta múltiplos departamentos com critérios/percentuais próprios, mesmo que só o Fisco/Contábil tenha regras cadastradas até agora.
- **8 models** (migrações `0024`–`0028`, `0033`–`0035`): `IndicadorDepartamento` (cadastro de departamentos — nome/ativo — usado pra escopar critérios, percentuais e metas de Departamento; ver "Departamento organizacional" abaixo), `IndicadorDepartamentoGerente` (relação gerente→departamento, mantida manualmente pela aplicação), `IndicadorPercentualTipo` (percentuais individual/grupo/departamento por tipo de colaborador **e por departamento**, histórico via `vigente_desde` — nunca editado in-place), `IndicadorCriterio` (cadastro genérico de critério: **departamento**/nome/grupo/peso/período/papel/`calculo_automatico`/`limiar_percentual`), `IndicadorApuracao` (uma apuração mensal — `competencia`, `status` `revisao`/`concluida`, as 2 planilhas anexadas, `avisos` de processamento), `IndicadorApuracaoColaborador` (um colaborador dentro de uma apuração, com `pct_individual`/`pct_grupo`/`pct_departamento` e respectivos flags `*_ajustado_manualmente`, mais `departamento` — FK pra `IndicadorDepartamento`, resolvida na criação via o gerente do colaborador, ver "Departamento organizacional" abaixo), `IndicadorApuracaoEmpresa` (uma empresa/honorário do colaborador naquele mês) e `IndicadorApuracaoResposta` (SIM/NÃO/NÃO FAZ/NÃO SE APLICA de um colaborador para um critério).
- **Tipo do colaborador é derivado por empresa, não é cadastro**: `TIPO_COLABORADOR_INDICADOR_CHOICES` (Contábil+Fiscal/Contador SC/Contador CC/Fiscal/Conciliador) — regra em `indicadores/tipos.py`, validada contra um recibo-modelo real (~99,99% de precisão no teste com 43 colaboradores).
- **3 critérios são calculados automaticamente** a partir da planilha "Serviços Tareffa" (`indicadores/entregas.py`/`pipeline.py`) — entrega de balancetes/liberações fiscais/conciliações no prazo, comparadas contra `IndicadorCriterio.limiar_percentual` pra decidir SIM/NÃO. Todo o resto é sempre marcação manual do RH (SIM/NÃO/NÃO FAZ/NÃO SE APLICA por critério, individual ou em lote). Um critério automático sem nenhum registro do serviço vira **NÃO SE APLICA**, não NÃO FAZ — permite deixar `papel_aplicavel` em branco nesses 3 critérios (um mesmo critério, ex. "balancete", se aplica a 3 tipos ao mesmo tempo, e `papel_aplicavel` só aceita um valor); quem não presta aquele serviço fica de fora do cálculo por conta própria (NÃO SE APLICA é excluído do denominador em `calculo.py`).
- **Fórmula**: `honorario_ajustado = honorario_empresa × pct_individual_do_colaborador`; `valor_individual = honorario_ajustado × percentual_individual(tipo)`; `valor_grupo`/`valor_departamento = valor_individual × percentual_grupo/departamento(tipo) × pct_grupo/departamento_do_colaborador`. `pct_individual` **não é só a média dos critérios Individual** — é a composição ponderada dos 3 níveis (Individual/Grupo/Departamento), cada um pesando conforme o peso médio dos seus próprios critérios aplicáveis na competência (`calculo._combina_niveis`/`_peso_medio_nivel`); só `pct_grupo`/`pct_departamento` continuam sendo a média simples dos próprios critérios, sem composição.
- **"Cada gerente representa um grupo", cada departamento representa um departamento** (não é redundante, ver abaixo): `pct_grupo` é conceitualmente compartilhado por todos os colaboradores com o mesmo `gerente` dentro da apuração, e `pct_departamento` é compartilhado por todos os colaboradores do mesmo `IndicadorDepartamento` (ver "Departamento organizacional" abaixo — não mais um valor único pra toda a apuração) — por isso não são ajustados colaborador a colaborador (`IndicadorApuracaoColaboradorViewSet` só cobre `pct_individual`); `IndicadorApuracaoViewSet.ajustar_grupo`/`recalcular_grupo`/`ajustar_departamento`/`recalcular_departamento` aplicam a mudança de uma vez a todo o grupo/departamento, mantendo os colaboradores em sincronia (`ajustar_departamento`/`recalcular_departamento` recebem `departamento` — o id do `IndicadorDepartamento` — no corpo, filtrando com um `.filter(departamento_id=...)` direto). A tela (`indicador-desempenho.js`) reflete isso com uma tabela de "Metas de Grupo e Departamento" no topo (uma linha de Departamento por `IndicadorDepartamento` + uma linha de Grupo por gerente dentro dele) separada da lista de colaboradores abaixo (que serve só pra revisão individual — percentual Individual, respostas de critério, recibo); botões de filtro por departamento (`#ind-filtro-departamento`, ver "Departamento organizacional" abaixo) restringem a tabela de Metas e a lista de colaboradores a um departamento de cada vez, sem afetar o cálculo de nenhuma meta. Colaborador cujo gerente não está mapeado a nenhum departamento cai num grupo "Sem departamento definido" (sem `<select>` de meta — não há `IndicadorDepartamento` pra aplicar). **A meta de Grupo/Departamento é sempre Sim/Não (100%/0%)**, nunca um percentual livre — decisão explícita do usuário ("será pago ou não") — por isso a coluna "Meta (%)" dessa tabela é um `<select class="ind-meta-select">` com só essas duas opções (`ehSim = valor >= 50` decide qual aparece selecionada ao carregar), não um campo de texto livre; o valor enviado a `ajustar-grupo`/`ajustar-departamento` é sempre `"100"` ou `"0"`. O percentual Individual de cada colaborador continua livre (é uma composição ponderada dos 3 níveis, pode legitimamente ser fracionário — ver acima).
- **Departamento organizacional** (`IndicadorDepartamento`/`IndicadorDepartamentoGerente`/`portal_api.indicadores.departamentos`) — substituiu, numa rodada posterior, o mecanismo de "setor" (coluna bruta "departamento" da planilha Tareffa + fusão automática Contabilidade/Fiscal→Fisco-Contábil + `IndicadorSetorApelido`, cadastro-exceção por colaborador). Agora **critérios e percentuais também são configurados por departamento** (não só as metas de Grupo/Departamento) — decisão explícita do usuário: a regra do Fisco/Contábil pode ser diferente da regra do Condomínio, por exemplo. `IndicadorDepartamento` (nome/ativo) é um cadastro simples, mantido pela própria aplicação (Configurações → Departamentos); a relação com gerentes (`IndicadorDepartamentoGerente`, `nome_gerente` único — um gerente pertence a só um departamento, mas um departamento pode ter vários gerentes, ex.: Fisco/Contábil tem "João Candido Rodrigues" **e** "Lhais Vergilio Delavy") também é mantida manualmente por ora — alimentar isso automaticamente a partir da planilha fica pra uma rodada futura (decisão explícita do usuário). Pra não obrigar o RH a redigitar nomes (arriscando um typo que faria uma apuração futura não casar com o departamento certo), o popup "Gerenciar Gerentes" (`indicador-desempenho.js`) mostra uma lista de **sugestões clicáveis** — `carregarGerentesSugeridos()` busca a apuração mais recente (`GET /api/indicadores-apuracoes/`, já ordenada por `-competencia`/`-criado_em`) e lista os nomes distintos de `colaborador.gerente` que ainda não estão em nenhum `IndicadorDepartamentoGerente`; clicar numa sugestão já cria a relação pra aquele departamento. É só um atalho de UI (não muda a origem do dado) — o campo de texto livre continua disponível pra gerentes que não apareceram na última apuração.
Resolução do departamento de um colaborador: `IndicadorApuracaoViewSet.create()` monta `mapa_gerentes` (`departamentos.carrega_mapa_gerentes()`, `{nome_gerente: departamento_id}`) uma vez e passa pro `pipeline.processa_apuracao()`, que resolve `departamento_id = mapa_gerentes.get(colaborador.gerente)` pra cada colaborador **antes** de decidir quais critérios automáticos calcular pra ele (críticos automáticos também são agrupados por `departamento_id` — `criterios_automaticos_por_departamento`, já que departamentos diferentes podem ter critérios/limiares diferentes). O resultado (`IndicadorApuracaoColaborador.departamento`, FK nullable) é um **retrato daquele momento** — mesmo espírito de `gerente`/`setor` antes dele: se a relação gerente→departamento mudar depois, apurações já criadas não mudam sozinhas. Colaborador cujo gerente não está mapeado a nenhum departamento fica com `departamento=None` e vira um aviso no processamento ("Gerente X não está associado a nenhum departamento..."), além de não ganhar nenhuma `IndicadorApuracaoResposta` (sem departamento, não há de onde vir nenhum critério). `IndicadorApuracaoColaboradorSerializer` expõe `departamento` (id) + `departamento_nome` (com fallback `None`, mesmo padrão de `criado_por_nome`).
**Migração em 3 passos** (mesmo padrão de qualquer FK obrigatória adicionada a uma tabela já populada): `0033` cria os 2 models novos + adiciona `departamento` nullable em `IndicadorCriterio`/`IndicadorPercentualTipo`/`IndicadorApuracaoColaborador` (e remove `setor`/`IndicadorSetorApelido`); `0034` (`RunPython`) cria o departamento "Fisco/Contábil" e aponta todo `IndicadorCriterio`/`IndicadorPercentualTipo` já existente pra ele (é literalmente o que a regra única representava até então); `0035` torna `departamento` obrigatório em `IndicadorCriterio`/`IndicadorPercentualTipo` (não em `IndicadorApuracaoColaborador`, que continua nullable). **Apurações criadas antes desta migração** (e qualquer apuração nova, até o admin mapear os gerentes relevantes em Configurações → Departamentos) ficam com `departamento` em branco em todos os colaboradores — precisam de um backfill pontual ou de serem reprocessadas depois que a relação gerente→departamento existir.
**Limitação conhecida, validada com dados reais**: como a resolução é por `gerente` (não por colaborador), dois subordinados diretos do mesmo gerente sempre caem no mesmo departamento — isso quebra o caso de uma gerente que supervisiona pessoas de **departamentos diferentes**. Ex. real: "Elizangela de Paula Kuhn" supervisiona diretamente os líderes de Fisco/Contábil (João Candido Rodrigues, Lhais Vergilio Delavy, Paloma Ramão, Elizangela dos Santos — departamento "Gerentes") **e** Luciane Gonzaga (que deveria cair em "Rocket", já que ela chefia esse outro departamento) — como todos compartilham o mesmo `gerente`, mapear "Elizangela de Paula Kuhn" → "Gerentes" também classifica Luciane Gonzaga como "Gerentes", não "Rocket". Não existe mais um mecanismo de exceção por colaborador individual (o antigo `IndicadorSetorApelido` cobria exatamente esse tipo de caso) — se isso for um problema real, precisa ser resolvido numa rodada futura (ex.: reintroduzindo uma exceção por nome de colaborador, por cima da relação gerente→departamento).
- **Detalhamento da composição no card do colaborador** (`portal_api.indicadores.calculo.composicao_individual`, exposto como o campo `composicao_individual` de `IndicadorApuracaoColaboradorSerializer`): reconstrói, só pra exibição, o percentual bruto de Individual (antes da composição) e o peso médio de cada um dos 3 níveis (`_peso_medio_nivel`) — dados que `recalcula_colaborador` calcula mas não persiste, por não precisar deles depois de gravar `pct_individual`. No cabeçalho do card (`indicador-desempenho.js`), essa linha ("Individual: X% (peso Y%) · Grupo: X% (peso Y%) · Departamento: X% (peso Y%)") fica ao lado do nome/gerente, numa coluna própria do grid centralizada — não embaixo — e cada um dos 3 níveis fica verde/vermelho conforme bateu 100% ou não; o "Total Indicador" (renomeado de "Individual", que é `pct_individual`, com o lápis de ajuste manual sempre ao lado do valor numa linha que não quebra) fica neutro, sem cor, pra não repetir a mesma informação 4 vezes. `composicao_individual()` usa `colaborador.respostas.all()` (não `.select_related("criterio")`) de propósito, pra reaproveitar o `prefetch_related("colaboradores__respostas__criterio")` que `IndicadorApuracaoViewSet.get_queryset()` aplica só na action `retrieve` — evita 1 query extra por colaborador ao abrir a tela de revisão.
- **Tabela "Metas de Grupo e Departamento" só tem uma forma de responder Sim/Não por critério** — a coluna "Meta (%)" (ajusta `pct_grupo`/`pct_departamento` direto). Existia um segundo `<select>` Sim/Não ao lado do texto de cada critério (bulk, via `aplicar-em-lote`), removido por ser redundante com o da direita; a lista de critérios ali agora é só informativa (nome + peso). Responder um critério específico continua possível por colaborador, dentro da lista de colaboradores abaixo (`renderRespostasGrupoHtml`).
- **"Corrigir Responsável"** (`#ind-corrigir-responsavel-btn`, popup próprio): busca uma empresa (por nome ou código, entre **todas** as empresas da apuração, não só as com problema de honorário — `empresasAgrupadasPorCodigo(() => true)`) e mostra, pra cada responsável dela (uma linha por `IndicadorApuracaoEmpresa`, ex.: "Valéria Bonete — Fiscal"), um `<select>` com os demais colaboradores da apuração pra reatribuir aquele papel. Reatribuir chama `POST /api/indicadores-apuracoes-empresas/{id}/trocar-responsavel/` (`IndicadorApuracaoEmpresaViewSet.trocar_responsavel`, serializer `IndicadorApuracaoEmpresaTrocarResponsavelSerializer` com `{colaborador_id}`), que só troca a FK `colaborador` da linha (`codigo_empresa`/`tipo`/honorário continuam os mesmos) e recalcula **os dois** colaboradores envolvidos (o que perdeu a empresa e o que ganhou) — validado no backend contra: colaborador de outra apuração, colaborador igual ao atual, e colaborador que já é responsável por essa mesma empresa/tipo (evitaria duas linhas duplicadas pra ele). O `<select>` exclui o colaborador atual das opções e nasce com um placeholder desabilitado ("Selecionar novo responsável...") pra nunca reatribuir sem escolha explícita.
- **Checklist de revisão do RH** (`IndicadorApuracaoColaborador.validado`, migração `0031`): um checkbox no início de cada card (`.ind-colaborador-card__validado`, primeira coluna do grid do cabeçalho), sem relação com nenhum cálculo — só ajuda o RH a controlar quem já conferiu numa apuração com muitos colaboradores. Marcado, a borda do card inteiro fica verde (`.ind-colaborador-card.is-validado`, mesma largura de sempre, só muda a cor, pra não deslocar layout). `POST /api/indicadores-apuracoes-colaboradores/{id}/marcar-validado/` (`marcar_validado`, serializer `IndicadorApuracaoColaboradorValidadoSerializer` com `{validado}`) só grava o campo, sem chamar `recalcula_colaborador`. Diferente dos outros ajustes desta tela, o frontend **não** recarrega a apuração inteira depois de marcar/desmarcar (`renderRevisao()`) — atualiza só o card clicado localmente, pra não fechar outros cards já expandidos nem perder a posição de rolagem no meio de uma conferência longa; erro de rede reverte o checkbox e o estado em memória (mesmo padrão de outros toggles imediatos do app, ex. inativar usuário). O `<label>` inteiro (não só o `<input>`) precisa ficar de fora do gate de clique que expande/recolhe o card no cabeçalho, senão um clique na área do label (fora do glifo do checkbox) expande/recolhe o card ao mesmo tempo que marca/desmarca o validado — resultado de como labels HTML disparam dois eventos de clique encadeados.
- **Forçar SIM num critério automático não vira 100% na média** — a média ponderada usa o percentual real medido (`_fracao_atingida` em `calculo.py`), mesmo que o RH marque SIM por cima; só critério manual (sem `percentual_calculado`) é binário SIM=100%/resto=0%. Pra dar crédito cheio apesar do percentual medido baixo, o RH ajusta o percentual agregado direto (nível 2 acima), não o critério.
- **`create()` é atômico**: `IndicadorApuracaoViewSet.create()` roda o pipeline inteiro (parse das 2 planilhas + persistência de colaboradores/empresas/respostas) dentro de `transaction.atomic()` — uma falha no meio (planilha fora do leiaute, overflow decimal) desfaz tudo no banco e apaga os 2 arquivos recém-gravados em `MEDIA_ROOT` (upload não é transacional), devolvendo 400 genérico.
- **`POST /api/indicadores-apuracoes/{id}/gerar/`** monta um **ZIP** com um PDF de recibo por colaborador (`indicadores/recibo.py`, `reportlab`) a partir do que já está salvo — não reprocessa as planilhas, reflete qualquer ajuste manual feito na revisão. Recibo é documento interno (só quem tem a permissão do RH acessa/baixa) — sem visão própria do colaborador no Portal nesta v1. Botão "Gerar Recibos" (`indicador-desempenho.js`) abre um modal antes de chamar o endpoint — mesmo componente de busca por nome + filtro por departamento + checklist (com "marcar todos os resultados da busca") do "Ajuste Indicador em Lote", só que já nasce com todo mundo marcado (reproduz o comportamento antigo de "gerar pra todos" sem precisar marcar um por um); desmarcar alguns permite gerar recibo avulso de um colaborador só, de alguns específicos, ou de um departamento inteiro. O endpoint recebe `colaborador_ids` (lista, opcional) e só marca a apuração como `concluida` quando o conjunto pedido bate com **todos** os colaboradores da apuração (sem `colaborador_ids`, ou uma seleção que cobre o total) — gerar um recibo avulso pra conferência não fecha a apuração inteira como se o mês estivesse todo revisado.
- **Layout do PDF do recibo** (`indicadores/recibo.py`): o banner "PERCENTUAL DO INDICADOR INDIVIDUAL" sempre mostra o percentual **efetivo/medido** (`calculo.composicao_individual()["total_calculado"]` — a composição dos 3 níveis recalculada na hora, ignorando qualquer ajuste manual), não `colaborador.pct_individual` puro — decisão explícita do usuário: se o RH/Diretoria sobrescreveu o Individual pra 100%, o banner precisa continuar mostrando o que o colaborador de fato atingiu (ex.: 74,29%), não o valor pago. Quando `pct_individual_ajustado_manualmente=True`, uma linha de detalhe abaixo do banner mostra "Percentual Individual Ajustado Pela Direção: **100,00%**." (rótulo renomeado de "ajustado manualmente pelo RH", com o valor ajustado ao lado — antes só dizia que tinha sido ajustado, sem mostrar pra quanto) — os dois números lado a lado deixam claro o que foi medido e o que foi pago. Tabela "Empresas": toda célula (antes só "Empresa" era `Paragraph`, o resto strings soltas) virou `Paragraph` com estilo de alinhamento próprio (`celula_centro`/`celula_direita`/`celula_negrito`/`celula_direita_negrito`, `_estilos()`) — string solta não quebra linha dentro da coluna, e com `ALIGN` à direita/centro um valor mais largo que a coluna (ex.: "Contador (com conciliador)" em Tipo, ou os totais em negrito, mais largos que a mesma string em peso normal) vazava visualmente por cima da célula vizinha em vez de quebrar linha — bug real visto com dados reais (coluna Tipo cobria "Hon. Ajustado"). Coluna "Tipo" ganhou um dicionário de labels curtos só pro PDF (`TIPO_LABEL_CURTO`: "Contador SC"/"Contador CC" em vez de "Contador (sem/com conciliador)" — mesma abreviação já usada informalmente neste documento) porque o label completo não cabia nem quebrando linha numa coluna estreita; a linha de total virou "Total do Indicador" (era "Total Resultado"). Larguras de coluna e padding lateral (`LEFTPADDING`/`RIGHTPADDING`, reduzidos de 6pt padrão do reportlab pra 3pt) ajustados pra caber os maiores valores reais vistos na apuração (ex.: R$ 28.023,16) numa linha só. `_moeda()` usa `&nbsp;` (não espaço comum) entre "R$" e o número — com espaço comum, quando o valor não cabia numa linha só, o reportlab quebrava exatamente ali, deixando "R$" sozinho numa linha acima do número; com espaço não separável, o "R$" fica sempre grudado à esquerda do número (mesmo que precise de mais espaço na coluna pra caber tudo numa linha, resolvido junto pelas larguras/padding acima). Rótulo da linha de detalhe é "Percentual individual ajustado pela direção" (minúsculo, só a primeira letra maiúscula — não "Percentual Individual Ajustado Pela Direção").
- **`aplicar_em_lote`** (`IndicadorApuracaoRespostaViewSet`, `POST /api/indicadores-apuracoes-respostas/aplicar-em-lote/`) aplica o mesmo valor a várias respostas de critério de uma vez — a "múltipla seleção" pedida pelo usuário na tela de revisão.
- **"Empresas sem Honorário"** (`#ind-empresas-sem-honorario-btn`, cor de atenção — `--danger`, mesma linguagem visual do input/selo de honorário não encontrado, **só enquanto houver alguma empresa pendente** — sem nada pra resolver, o botão perde a classe `.ind-empresas-sem-honorario-btn` (volta a `.btn-outline` neutro) e o texto vira "Visualizar Empresas com Honorário Ajustado (N)", apontando direto pra revisão do que já foi ajustado — substituiu o antigo checkbox "Só com honorário não encontrado" que filtrava a lista de colaboradores): abre um modal que agrupa por `codigo_empresa` todas as `IndicadorApuracaoEmpresa` com `honorario_nao_encontrado=True` da apuração — a mesma empresa pode aparecer sob mais de um colaborador (um responsável pelo balancete, outro pela liberação fiscal, outro pela conciliação financeira), mas o honorário é da empresa, não da pessoa. Preencher um valor ali chama `POST /api/indicadores-apuracoes/{id}/ajustar-honorario-empresa/` (`IndicadorApuracaoViewSet.ajustar_honorario_empresa`, serializer `IndicadorApuracaoAjusteHonorarioEmpresaSerializer` com `{codigo_empresa, honorario}`), que atualiza **todas** as linhas daquele código nesta apuração de uma vez (recalculando cada colaborador afetado) — diferente de `PATCH /api/indicadores-apuracoes-empresas/{id}/` (ainda existe, ajusta só uma linha por id, usado direto na tabela "Empresas" de dentro do card do colaborador). Os dois caminhos (linha única e em lote) marcam `honorario_ajustado_manualmente=True` na(s) linha(s) afetada(s) — fica permanente (sem UI de reverter, diferente de `pct_individual_ajustado_manualmente`/etc., já que não existe um "automático" pra voltar quando o código nunca casou com a planilha) e vira uma nota "honorário ajustado manualmente" (cor `--accent`) ao lado do valor, na tabela "Empresas" de dentro do card do colaborador — visível só depois que `honorario_nao_encontrado` já foi resolvido (os dois nunca aparecem juntos). Essa tabela também passou a listar por `codigo_empresa` (`IndicadorApuracaoEmpresa.Meta.ordering`, migração `0029`), não mais por nome. Como `codigo_empresa` é `CharField`, ordenar só por ele é ordem alfabética, não numérica — "80"/"503" apareciam depois de "2134" (o caractere `'8'`/`'5'` é "maior" que `'1'`/`'2'`, mesmo o número sendo menor). Corrigido (migração `0030`) ordenando primeiro pelo **tamanho** da string (`Length("codigo_empresa")`) e só depois pelo valor — reproduz a ordem numérica certa pra códigos sem zero à esquerda (string mais curta = número menor, sempre) sem converter pra inteiro, o que quebraria com erro de banco se algum código um dia não fosse só dígitos.
- **"Empresas ajustadas manualmente"** é uma **segunda seção dentro do mesmo popup** "Empresas sem Honorário" — não um segundo botão/modal (revertido de propósito: nasceu como um botão separado, "Verificar Empresas Ajustadas Manualmente", e o usuário pediu pra unificar num popup só, "facilitando a usabilidade da ferramenta"). Fica **escondida por padrão**, atrás de um botão de largura cheia no final da lista principal (`#ind-empresas-ajustadas-toggle-btn`, `.ind-esh-toggle-btn`, com contador — "Visualizar"/"Ocultar Empresas Ajustadas Manualmente (N)") — pedido explícito do usuário logo depois de testar a versão anterior (as duas seções sempre visíveis de uma vez): a lista secundária só deve aparecer sob demanda, no final do modal. Lista, também agrupada por `codigo_empresa`, as empresas com `honorario_ajustado_manualmente=True` — permite **corrigir** um valor já ajustado (campo já vem preenchido com o honorário atual, ao contrário da lista principal, que começa em branco). Reaproveita o mesmo endpoint `ajustar-honorario-empresa` — o filtro do backend cobre `Q(honorario_nao_encontrado=True) | Q(honorario_ajustado_manualmente=True)`, nunca uma empresa cujo honorário só veio certo da planilha e nunca foi mexido. As duas listas compartilham as funções de agrupamento/renderização/ordenação (`empresasAgrupadasPorCodigo`, `renderEmpresaGrupoItemHtml`) em `indicador-desempenho.js`, parametrizadas só pelo filtro; `renderEmpresasHonorario()` sempre re-renderiza a lista principal e só re-renderiza a de "ajustadas" **se a seção já estiver aberta** (ao abrir o popup, essa seção sempre volta a fechar) — necessário porque corrigir uma empresa "sem honorário" faz ela migrar pra lista de "ajustadas" na hora, e essa migração só precisa refletir de imediato se o usuário já estiver olhando pra ela. As duas ficam dentro de um único wrapper que rola (`.ind-esh-scroll`), com título e "Fechar" sempre visíveis fora dele (mesmo `max-height:85vh` do popup). Em cada item, o código aparece **antes** do nome da empresa no cabeçalho (`.ind-esh-codigo` seguido de `.ind-esh-nome`), mesma ordem da tabela "Empresas" do colaborador.
- **"Ajuste Indicador em Lote"** (`#ind-lote-global-btn`, `indicador-desempenho.js`): modal separado do anterior — ajusta `pct_individual` (não critérios) de vários colaboradores **selecionados por nome** de uma vez, pra dois casos binários só: "Ajustar" (`#ind-lote-global-ajustar-btn`, aplica `pct_individual=100` a todos, via `PATCH /api/indicadores-apuracoes-colaboradores/{id}/`) ou "Reverter" (chama a action `recalcular` de cada um, voltando ao cálculo automático) — sem campo de percentual livre. Os dois botões são `btn-solid` (mesma cor) — só "Cancelar" fica `btn-outline`, já que as duas ações são igualmente "reais", não uma primária e uma secundária. Não existe endpoint de lote dedicado pra isso; o frontend dispara um PATCH/POST por colaborador em paralelo (`Promise.all`).
- **Percentuais/critérios são cadastro editável pela tela**, não hardcoded — decisão explícita do usuário pra não fixar no código números incertos vindos da planilha antiga já quebrada. Primeiro histórico populado via `python manage.py seed_indicador_desempenho` (idempotente), com os valores exatos da planilha antiga (`vigente_desde` fixado em 01/01/2024 por falta de data documentada — ajustar se o usuário informar a data real).
- **Bugs de robustez corrigidos ao testar com 43 colaboradores reais**: `openpyxl.load_workbook(..., read_only=True)` precisa de `.close()` explícito (`indicadores/leiaute.py`), senão o Windows mantém o upload memory-mapped e bloqueia excluir a apuração depois; campos percentuais precisaram de `max_digits=7` (não 6) — qualquer `DecimalField` que representa um percentual "de 0 a 100" precisa de `max_digits >= decimal_places + 3` pra caber o "100" exato sem `DataError: numeric field overflow`.
## Importação de Plano de Saúde (Utilitários)
Primeira e única aplicação dentro de "Utilitários" (os placeholders "Conversor de Arquivos"/"Calculadora Fiscal" foram removidos do menu — decisão explícita do usuário, não recriar sem confirmar de novo) — importa o relatório de faturamento de uma operadora de plano de saúde/odontológico (Amil, Unimed, ...) e gera o arquivo de lançamento no leiaute fixo do Questor, mais um relatório de auditoria do que não pôde ser lançado automaticamente. Ao contrário dos módulos com tela administrável (Links & Ferramentas, Acessos Gerais, Ramais), essa ferramenta usa permissão de **toggle único** (`{"key": "importacao-plano-saude", "label": "..."}`, entrada flat em `catalogo.MODULE_APPS["utilitarios"]`, sem par visualizar/editar) — quem tem acesso pode fazer todo o fluxo (criar, revisar/editar, gerar), sem conceito de "dono" da importação (mesmo espírito compartilhado de `LinkFerramenta`/`AcessoGeral`). Por ser um app flat, não precisou de nenhum override em `seed_portal.py` (esse cuidado só existe pra pares visualizar/editar).
**A lógica de negócio em si não nasceu neste projeto** — veio de um pipeline Python já testado e documentado em `projects/importacao-planos-saude.skill` (arquivo `.skill`, é um zip — `SKILL.md` + `scripts/`), com um protótipo funcional em `projects/project/` (CLI `main.py`, nunca tocado pelo Portal, fica só como referência/histórico). Esse pipeline foi portado quase 1:1 para dentro do Django em **`portal_api/planos_saude/`** (pacote Python puro, sem depender do ORM):
```
portal_api/planos_saude/
├── modelos.py Lancamento, Individuo, LinhaSistema, ItemAuditoria (dataclasses)
├── matcher.py casa_individuos_com_planilha() — casamento por CPF ou por nome
├── leiaute_sistema.py CABECALHO, le_planilha_padrao(), formata_valor_br()
├── pipeline.py OPERADORAS (registro), processa_importacao() — orquestração, chamada pela view
└── operadoras/
├── base.py OperadoraParser (interface)
├── amil/odonto_mensalidade.py Amil Odonto (PDF via pdfplumber, só mensalidade, casamento por CPF)
├── unimed/saude.py Unimed (CSV, mensalidade+coparticipação, casamento por nome)
├── itamed/saude.py Itamed Saúde (PDF via pdfplumber, mensalidade+coparticipação, casamento por nome)
├── dental_uni/odonto_mensalidade.py Dental Uni Odonto (PDF via pdfplumber, só mensalidade, casamento por nome)
└── unimed_oeste_pr/saude.py Unimed Oeste do Paraná (PDF via pdfplumber, mensalidade+coparticipação por texto da descrição, casamento por nome)
```
Pra adicionar uma operadora nova: criar `operadoras/<nome>/<arquivo>.py` implementando `OperadoraParser.extrai()` (devolve `(List[Individuo], List[ItemAuditoria])`) e registrar em `pipeline.OPERADORAS`. **Antes de escrever o parser, ler `projects/importacao-planos-saude.skill`** — documenta decisões de negócio já validadas com o cliente (ex.: nome divergente nunca é resolvido por aproximação, valor final negativo vai pra auditoria, um mesmo beneficiário pode aparecer em várias linhas/rubricas e precisa ser somado) que não devem ser reinterpretadas sem confirmar de novo.
**Diferença deliberada em relação ao pipeline original**: lá, o valor do mês sempre gravava na coluna `VALOR` (desconto do empregado), nunca em `VALOREMPRESA` — regra fixa. Aqui, o usuário escolhe na tela de nova importação, **por tipo de lançamento (mensalidade/coparticipação) e por tipo de beneficiário (titular/dependente)** — quatro combinações independentes, ex.: mensalidade do titular custeada pela empresa e mensalidade do dependente descontada do empregado —, uma de três regras de custeio: "Custeado pela empresa" (`{"modo": "empresa"}`), "Descontado do empregado" (`{"modo": "empregado"}`, o comportamento antigo — nomenclatura "empregado", não "funcionário", pra não confundir com `NOMEFUNC`/`CPFFUNC` do leiaute do Questor, que é outra coisa) ou "Regra específica" (`{"modo": "especifica", "limite_valor": float|None, "percentual": float|None}`). Na regra específica, `limite_valor` é um teto de quanto a empresa cobre (o excedente vira desconto do empregado) e `percentual` é a fração do valor do mês custeada pela empresa (o resto vira desconto) — o usuário pode preencher só um dos dois ou os dois juntos; quando os dois vêm preenchidos, prevalece o que resultar no **menor** valor custeado pela empresa (mais restritivo), decisão explícita do usuário. Essa divisão é calculada por `matcher._calcula_valores(valor_total, regra)` (chamada por `_aplica_regra_custeio`, que grava `valor_empresa`/`valor` **os dois juntos** a partir do mesmo `valor_total`) — note que `valor_empresa` é arredondado primeiro e `valor` é derivado como o complemento exato (`valor_total - valor_empresa`, também arredondado), nunca os dois arredondados de forma independente, senão a soma dos dois podia ficar 1 centavo a mais/menos que o valor original (ex.: 50% de 51,69 tem que fechar em 25,84 + 25,85 = 51,69, não 25,85 + 25,85). Qual das duas regras (titular ou dependente) usar em cada `Individuo`/`LinhaSistema` é resolvido por `matcher._regra_para_pessoa(regra_por_pessoa, tipo_pessoa)` — `tipo_pessoa` 'T' cai em "titular", 'D'/'A' caem em "dependente" (mesmo critério de "D e A tratados igual" já usado no resto do leiaute) — chamada nos dois pontos de aplicação de `_casa_por_cpf`/`_casa_por_nome` antes de `_aplica_regra_custeio`.
### Modelos (`portal_api/models.py`)
- `ImportacaoPlanoSaude`: uma execução da ferramenta — `operadora`/`nome_operadora`, `tipos_lancamento` (JSONField, lista), `custeio_por_tipo` (JSONField, `{"mensalidade": {"titular": {"modo": "empresa"|"empregado"|"especifica", "limite_valor": float|None, "percentual": float|None}, "dependente": {...}}, "coparticipacao": {...}}` — ver regra de custeio acima), os dois arquivos anexados (`planilha_padrao`/`arquivo_operadora`, `FileField` com o mesmo padrão de validator de tamanho de `LinkFerramenta.icone`, só que 15MB em vez de 2MB — são documentos reais, não ícones), `status` (`revisao`/`concluida`), `criado_por`, `criado_em`/`concluida_em`. **Com histórico**: decisão explícita do usuário — cada importação fica salva (quem fez, quando, arquivos), não é um fluxo descartável.
- `ImportacaoPlanoSaudeLinha`: uma linha da planilha padrão já casada com o valor do mês (espelha `LinhaSistema` campo a campo) — **todos os campos são editáveis** na tela de revisão antes de gerar o CSV (decisão explícita do usuário, não só os valores). `valor`/`valor_empresa` ficam como `CharField` no mesmo formato string do pipeline (`"51,69"`/`"0"`), não `DecimalField`, pra manter fidelidade 1:1 com o CSV final sem risco de arredondamento.
- `ImportacaoPlanoSaudeAuditoria`: espelha `ItemAuditoria` — os campos extraídos do arquivo da operadora (`motivo`/`nome`/`valor`/`detalhe`...) são read-only na tela; `resolvida`/`linha_vinculada` são a exceção, graváveis via a resolução manual (ver "Resolução manual de auditoria por nome" abaixo). `MOTIVOS_RESOLVIVEIS = ("NOME_DIVERGENTE", "NAO_CADASTRADO")` (atributo de classe) é a lista dos dois motivos "de leitura/grafia de nome" que aceitam esse fluxo — `VALOR_NEGATIVO`/`TIPO_INVALIDO` são outra categoria de problema (valor real negativo, tipo de despesa não mapeado) e não têm solução por "essa é a mesma pessoa".
- `RegraCusteioPlanoSaude`: regra de custeio salva pra reaplicar em importações futuras (ex.: "092 - Unimed") — ver "Regras de custeio salvas" abaixo. Lista compartilhada (sem FK pra `ImportacaoPlanoSaude`), mesmo espírito de `LinkFerramenta`/`AcessoGeral`.
### Fluxo e endpoints
`ImportacaoPlanoSaudeViewSet` (`/api/importacoes-plano-saude/`, `PermissaoApp("utilitarios", "importacao-plano-saude")` pra todos os métodos):
- `create()` (multipart, `ImportacaoPlanoSaudeCreateSerializer` valida a entrada) salva o model (isso já grava os 2 arquivos em `MEDIA_ROOT`) e roda `pipeline.processa_importacao()` **de forma síncrona** usando `instance.arquivo_operadora.path`/`instance.planilha_padrao.path` — sem fila/Celery, o arquivo típico processa em menos de um request. Se o processamento falhar (PDF num layout desconhecido etc.), apaga os arquivos recém-salvos + o registro órfão e devolve 400.
- `GET /operadoras/` (`@action` sem detail) devolve `pipeline.lista_operadoras()` — fonte única pro `<select>` do formulário, sem duplicar a lista em JS.
- `POST /{id}/gerar/` monta o(s) CSV(s) a partir das **linhas já salvas** (isto é, já com qualquer edição feita na revisão — não reprocessa os arquivos originais) usando `leiaute_sistema.CABECALHO`; 1 tipo de lançamento vira um `.csv` direto, 2 tipos (mensalidade + coparticipação) viram um `.zip` com um `.csv` por tipo (`zipfile` em memória). Pode ser chamada de novo pra regerar depois de mais edições — não bloqueia edição subsequente.
`ImportacaoPlanoSaudeLinhaViewSet` (`/api/importacoes-plano-saude-linhas/{id}/`, só GET/PATCH): edição de uma linha por vez, disparada por `blur`/`change` de cada `<input>` na tela de revisão — mesma permissão de toggle único, sem checagem de "dono".
`ImportacaoPlanoSaudeAuditoriaViewSet` (`/api/importacoes-plano-saude-auditoria/{id}/resolver/`, só `POST`) — ver seção própria abaixo.
### Resolução manual de auditoria por nome
Quando o casamento por nome falha (`NOME_DIVERGENTE`/`NAO_CADASTRADO` — ver `matcher.py`, "nunca resolvido por aproximação automática"), o colaborador pode confirmar manualmente que aquele item **é** uma pessoa específica já presente na planilha padrão, em vez de deixar o lançamento parado em auditoria pra sempre. Não é fuzzy matching nem aproximação automática — é sempre uma confirmação humana, explícita, item por item; a regra de "nome exato ou vai pra auditoria" do `matcher.py` continua intocada.
- **Endpoint**: `POST /api/importacoes-plano-saude-auditoria/{id}/resolver/` com `{"linha_id": <id>}`. Validações em `ImportacaoPlanoSaudeAuditoriaViewSet.resolver` (views.py): o item precisa ter um motivo em `MOTIVOS_RESOLVIVEIS` e ainda não estar `resolvida` (idempotente — não dá pra resolver de novo, nem trocar o vínculo depois); a linha escolhida precisa (a) ser da mesma importação e do mesmo `tipo_lancamento` do item; (b) ser do mesmo "lado" — titular pra item `tipo="T"`, dependente pra `tipo!="T"` (D/A) — comparando `linha.nome_dependente`/`cpf_dependente` vazios ou não; (c) **ainda estar em branco** (`valor == valor_empresa == "0"`), decisão explícita do usuário pra nunca sobrescrever sem querer um lançamento que já casou automaticamente com outra pessoa do arquivo da operadora.
- Ao vincular, o `valor` do item de auditoria é dividido em `valor_empresa`/`valor` pela mesma regra de custeio já salva em `ImportacaoPlanoSaude.custeio_por_tipo[tipo_lancamento]` para aquele tipo de pessoa (titular/dependente) — `matcher.valores_formatados_para_pessoa(valor_total, regra_por_pessoa, tipo_pessoa)` é o único ponto de entrada público do módulo pra isso, reaproveitando as mesmas `_regra_para_pessoa`/`_calcula_valores` do fluxo automático (não existe uma segunda fórmula "manual").
- O item **nunca é apagado nem some da lista**: fica marcado `resolvida=True` + `linha_vinculada` (FK), e a tela mostra um selo "Resolvido — <nome>" (verde, mesma linguagem visual de `.status-pill--ativo`) no lugar do botão "Vincular pessoa" — mantém o rastro de que aquele valor entrou por confirmação manual, não pelo casamento automático (mesma filosofia de histórico completo do resto do módulo). `get_resumo_por_tipo` (serializers.py) só conta itens **não resolvidos** em `total_auditoria`, pra não inflar o contador de pendências com algo que já foi lançado.
- **Frontend** (`importacao-plano-saude.js`): a coluna "Ação" da aba Auditoria (`panelHtmlAuditoria()`) mostra o botão "Vincular pessoa" só quando `PID_IPS_MOTIVOS_RESOLVIVEIS.includes(item.motivo)` e `!item.resolvida`. O modal `#ips-vincular-modal` lista candidatos **sem nenhuma chamada de API nova** — filtra em memória a partir de `importacaoAtual.linhas` (já carregado na revisão) por `tipo_lancamento` igual, "lado" (titular/dependente) igual e ainda em branco (`candidatosVincular()`), com uma caixa de busca por nome (`renderVincularLista()`, mesmo componente `.checklist-box`/`.checklist-search` de outras telas, aqui com `<input type="radio">` — seleção única, não múltipla). Confirmar chama `pidResolverAuditoriaPlanoSaude()` e refaz `pidFetchImportacaoPlanoSaude` pra recarregar `importacaoAtual` (mesmo padrão de "adicionar/remover linha" já usado na página) antes de re-renderizar as abas — a tabela do próprio tipo de lançamento também reflete o novo valor lançado, não só a aba Auditoria.
### Pré-validação de arquivo ao anexar (tela de Nova Importação)
Antes de existir isso, os dois arquivos (planilha padrão + arquivo da operadora) só eram validados juntos, no `create()`, e um erro de formato virava a mensagem genérica "O formato de um dos arquivos não está conforme o esperado" — sem dizer qual dos dois. Agora cada anexo é validado sozinho, no momento em que é selecionado, reaproveitando exatamente o mesmo parser que `create()` usaria — sem duplicar nenhuma regra de leiaute em JS (o parsing de PDF/CSV é Python-only, então isso teria que ser uma chamada ao servidor de qualquer forma).
- **Endpoint**: `POST /api/importacoes-plano-saude/validar-arquivo/` (multipart `{tipo: "planilha"|"operadora", arquivo, operadora?}`) — sempre `200 {"valido": bool, "mensagem": str}`, nunca um erro HTTP pra "arquivo errado" (esse é um resultado esperado da validação, não uma falha de requisição; só falta de `arquivo`/`tipo` inválido/`operadora` ausente quando `tipo="operadora"` vira 400 de verdade). `_valida_planilha_padrao()` roda `leiaute_sistema.le_planilha_padrao()`; `_valida_arquivo_operadora()` roda `OPERADORAS[operadora_key]["parser"]().extrai()` — os dois gravam o upload num arquivo temporário (`_salva_arquivo_temporario`, `tempfile.NamedTemporaryFile`) só porque essas funções esperam um caminho de arquivo, não um objeto de upload em memória, e apagam o temporário no `finally`; **nada é persistido**. Qualquer exceção do parser (coluna faltando, layout de PDF não reconhecido, CSV com delimitador errado — inclusive o caso real já visto de export com `\t` em vez de `;`) vira `valido=False` com uma mensagem específica pra aquele arquivo; 0 linhas/indivíduos extraídos (arquivo no formato certo mas vazio) também vira `valido=False`.
- **Frontend** (`importacao-plano-saude.js`): `criarValidadorArquivo()` é a fábrica reaproveitada pelos dois campos (`validadorPlanilha`/`validadorArquivo`) — no `change` do `<input type="file">`, chama `pidValidarArquivoPlanoSaude()` e mostra o resultado abaixo do campo (`.ips-file-field__status`, cores diferentes pra pendente/ok/erro). Cada campo ganhou um botão de remover (`.ips-file-field__remove`, ícone X — só aparece com um arquivo anexado) que limpa o `<input>` e o estado de validação, pro colaborador poder tentar outro arquivo sem precisar recarregar a página quando o anexado voltar como divergente. Trocar a operadora depois de já ter anexado o arquivo dela (`formOperadora` `change`) reexecuta a validação automaticamente (`revalidarSeAnexado()`) — o parser usado depende de qual operadora está selecionada, então um arquivo validado contra a operadora errada precisa ser checado de novo. O botão "Processar" bloqueia (`ehInvalido()`) se qualquer um dos dois arquivos já voltou `valido=False` — mas isso é só uma segunda barreira de UX; o `create()` no servidor continua sendo a validação real e definitiva.
Gerar o arquivo é um download binário (CSV ou ZIP), não JSON — por isso `pidGerarArquivoPlanoSaude()` não usa `pidApiRequest` (que sempre tenta `JSON.parse`); faz um `fetch` manual reaproveitando `pidEnsureCsrfCookie`/`pidGetCookie`/`pidErrorMessageFrom` de `api.js` (funções globais na página) e dispara o download via `URL.createObjectURL`.
### Regras de custeio salvas
Substituiu o antigo par de botões "Exportar regra"/"Importar regra" (baixava/lia um `.json` manualmente, sem nenhuma persistência) por um banco de regras de verdade no Postgres (`RegraCusteioPlanoSaude`) — pedido explícito do usuário pra poder nomear uma regra (ex.: "092 - Unimed"), escolhê-la numa lista em importações futuras, editá-la depois e anotar uma observação livre (ex.: "Empresa não desconta plano do empregado XX").
- **Campos**: `nome` (obrigatório), `operadora` (opcional — a `key` de `planos_saude.pipeline.OPERADORAS`, não o label; só usada pra pré-selecionar o `<select>` de operadora ao aplicar a regra, nunca bloqueia aplicar uma regra com uma operadora diferente da atual), `tipos_lancamento`/`custeio_por_tipo` (exatamente o mesmo formato dos campos homônimos de `ImportacaoPlanoSaude`, ver acima) e `observacoes` (texto livre).
- **Validação reaproveitada, não duplicada**: `RegraCusteioPlanoSaudeSerializer.validate()` e `ImportacaoPlanoSaudeCreateSerializer.validate()` chamam a mesma função módulo-level `_monta_regra_custeio()` (`serializers.py`) pra validar/parsear cada combinação tipo×pessoa — sem isso, a regra de negócio de custeio (parsing BR, faixa 0–100 do percentual, "ao menos um de limite/percentual") viveria duplicada em dois serializers e podia divergir com o tempo. A única diferença entre os dois pontos de entrada é o formato de payload: `ImportacaoPlanoSaudeCreateSerializer` recebe campos multipart achatados (`custeio_mensalidade_titular`, `limite_valor_mensalidade_titular`...), `RegraCusteioPlanoSaudeSerializer` recebe o `custeio_por_tipo` já aninhado como JSON puro.
- **`limite_valor`/`percentual` sempre em texto BR na entrada, float|None persistido**: igual ao resto do módulo, esses dois campos chegam como string BR (`"150,00"`) tanto no create de uma importação quanto no banco de regras — mas uma regra salva, uma vez lida de volta pelo `GET`, já vem com esses valores como `float` (formato final persistido). Pra `RegraCusteioPlanoSaudeSerializer` aceitar os dois formatos sem corromper o valor (`"150.0"` seria lido errado como 15000 por `parse_valor_br`, que só entende separador de milhar `.`/decimal `,`), `_valor_custeio_para_texto_br()` normaliza um float de volta pra texto BR (`formata_valor_br`) antes de repassar pro parser — isso é o que permite reenviar uma regra sem edição (ex.: só mudando o nome) sem precisar reformatar nada no frontend.
- **Frontend** (`importacao-plano-saude.js`, seção "Regra de custeio salva" — **primeiro** campo do formulário de Nova Importação, antes até de "Operadora": decisão explícita do usuário, já que aplicar uma regra já preenche a operadora junto, então escolher a regra é o primeiro passo natural do fluxo, não um apêndice no final): um combobox pesquisável (`#ips-regra-combo` — `<input>` `#ips-regra-search` + lista flutuante `#ips-regra-combo-list`, filtra por nome a cada tecla; era um `<select>` simples, trocado quando o banco de regras cresceu o bastante pra não caber numa lista sem busca) + "Aplicar" preenche o formulário inteiro (tipos de lançamento + custeio de cada combinação + operadora, se ainda existir na lista) a partir de uma regra salva — os campos continuam 100% editáveis depois, é só um preenchimento em massa (`aplicarCusteio()`/`aplicarRegraNoFormulario()`), mesmo espírito do antigo "Importar regra". `regraSelecionadaId` (JS) rastreia o que está de fato escolhido no combobox — digitar de novo no campo invalida a seleção anterior até o usuário clicar numa regra da lista, pra "Aplicar" nunca usar uma regra desatualizada em relação ao texto exibido. "Salvar regra atual..." (`#ips-regra-salvar-btn`) abre um modal (`#ips-regra-save-modal`) pra nomear/descrever a configuração atualmente preenchida (validada antes com a mesma `mensagemErroCusteio()` usada pelo botão "Processar", reaproveitada pelas duas ações) — **editar uma regra existente é literalmente aplicá-la, ajustar o que quiser no formulário, e salvar de novo**: o modal nasce em modo "atualizar a regra selecionada" sempre que a regra atualmente refletida no formulário (`regraAplicadaId`) ainda existir, com uma checkbox pra optar por "criar uma nova regra" em vez de sobrescrever. "Ver regras salvas" (`#ips-regras-modal`) lista todas as regras (nome, operadora, tipos, observações) com ações de Aplicar/Excluir — não duplica a grade de custeio num modal separado, de propósito, pra não manter dois lugares editáveis da mesma coisa.
## 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), 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 7 páginas (`perfis-acesso.html`, `usuarios.html`, `links-ferramentas.html`, `acessos-gerais.html`, `ramais.html`, `calendario-individual.html`, `importacao-plano-saude.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) **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 em `usuarios.html` e no modal "Gerenciar Usuário" de todo shell, ver seção "Liderança"). |
| `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`; também tem `#portal-title` (fonte "Bree Serif" do título "Portal De Paula" no topbar), que não é widget mas mora aqui por ser o único CSS próprio da página. |
| `login.css` | Só usado em `index.html`; `.login-card__title` ("Portal De Paula") usa a mesma fonte "Bree Serif" do `#portal-title` de `portal.html`. |
| `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 "Ramais" abaixo), 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). |
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.

0
config/__init__.py Normal file
View File

7
config/asgi.py Normal file
View File

@ -0,0 +1,7 @@
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
application = get_asgi_application()

106
config/settings.py Normal file
View File

@ -0,0 +1,106 @@
import os
from pathlib import Path
from dotenv import load_dotenv
BASE_DIR = Path(__file__).resolve().parent.parent
load_dotenv(BASE_DIR / ".env")
SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", "django-insecure-troque-esta-chave-em-producao")
DEBUG = os.environ.get("DJANGO_DEBUG", "True") == "True"
# ALLOWED_HOSTS = [h.strip() for h in os.environ.get("DJANGO_ALLOWED_HOSTS", "localhost,127.0.0.1").split(",")]
ALLOWED_HOSTS = ['*']
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"rest_framework",
"portal_api",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "config.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
WSGI_APPLICATION = "config.wsgi.application"
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": os.environ.get("DB_NAME", "portal_de_paula"),
"USER": os.environ.get("DB_USER", "postgres"),
"PASSWORD": os.environ.get("DB_PASSWORD", ""),
"HOST": os.environ.get("DB_HOST", "localhost"),
"PORT": os.environ.get("DB_PORT", "5432"),
}
}
AUTH_USER_MODEL = "portal_api.Usuario"
AUTH_PASSWORD_VALIDATORS = [
{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]
LANGUAGE_CODE = "pt-br"
TIME_ZONE = "America/Sao_Paulo"
USE_I18N = True
USE_TZ = True
STATIC_URL = "static/"
STATICFILES_DIRS = [BASE_DIR / "static"]
STATIC_ROOT = BASE_DIR / "staticfiles"
# Upload de usuário (hoje só os ícones dos cartões de Links & Ferramentas).
MEDIA_URL = "media/"
MEDIA_ROOT = BASE_DIR / "media"
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.SessionAuthentication",
],
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
],
}
# Sessão + CSRF: frontend e backend na mesma origem (Django serve os dois),
# então não precisamos de CORS nem de SameSite=None/Secure.
CSRF_COOKIE_HTTPONLY = False
SESSION_COOKIE_SAMESITE = "Lax"
CSRF_COOKIE_SAMESITE = "Lax"

40
config/urls.py Normal file
View File

@ -0,0 +1,40 @@
"""
Além de /api/, este urls.py também serve o frontend (templates/*.html) —
front e back na mesma origem, para autenticação por sessão/cookie funcionar
sem CORS. Os estáticos (static/) são servidos por django.contrib.staticfiles
automaticamente em DEBUG (ver STATICFILES_DIRS em settings.py); em produção,
rode `manage.py collectstatic` e sirva STATIC_ROOT via whitenoise/nginx.
"""
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import include, path
from django.views.generic import TemplateView
PAGINAS_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",
]
urlpatterns = [
path("admin/", admin.site.urls),
path("api/", include("portal_api.urls")),
path("", TemplateView.as_view(template_name="index.html")),
]
urlpatterns += [
path(pagina, TemplateView.as_view(template_name=pagina)) for pagina in PAGINAS_FRONTEND
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

7
config/wsgi.py Normal file
View File

@ -0,0 +1,7 @@
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
application = get_wsgi_application()

0
database/__init__.py Normal file
View File

100
database/connection.py Normal file
View File

@ -0,0 +1,100 @@
from dataclasses import dataclass, field
from collections.abc import Sequence
from typing import Any, Literal, ClassVar, overload
from sqlalchemy import create_engine, text, Row, RowMapping
from sqlalchemy.engine import Engine
from database.settings import Settings
from database.query import SQLQuery
@dataclass
class DatabaseConnection:
connection: str
engine: Engine = field(init=False)
_settings: ClassVar[Settings] = Settings()
def __post_init__(self) -> None:
self.engine = create_engine(
url=DatabaseConnection._settings.get_url_database(self.connection),
pool_recycle=3600,
pool_pre_ping=True,
echo=False
)
def execute(self, sql: str, params: dict[str, Any] | None = None) -> None:
"""Abre uma transação, executa e commita um INSERT/UPDATE/DELETE sem retorno."""
with self.engine.begin() as conn:
conn.execute(text(sql), params)
def execute_query(self, query: SQLQuery) -> None:
"""Mesmo que execute(), recebendo um SQLQuery em vez de sql/params separados."""
self.execute(query.sql, query.params)
@overload
def select_rows(self, sql: str, params: dict[str, Any] | None = None, *, one: Literal[True]) -> Row[Any] | None: ...
@overload
def select_rows(self, sql: str, params: dict[str, Any] | None = None, *, one: Literal[False] = False) -> Sequence[Row[Any]]: ...
def select_rows(self, sql: str, params: dict[str, Any] | None = None, *, one: bool = False) -> Row[Any] | Sequence[Row[Any]] | None:
"""Abre uma conexão e executa um SELECT que retorna tuplas (Row). \n
Parâmetro one indica se deve retornar um Row ou uma Sequence de Rows."""
with self.engine.connect() as conn:
result = conn.execute(text(sql), params)
return result.fetchone() if one else result.fetchall()
@overload
def select_rows_query(self, query: SQLQuery, *, one: Literal[True]) -> Row[Any] | None: ...
@overload
def select_rows_query(self, query: SQLQuery, *, one: Literal[False] = False) -> Sequence[Row[Any]]: ...
def select_rows_query(self, query: SQLQuery, *, one: bool = False) -> Row[Any] | Sequence[Row[Any]] | None:
"""Mesmo que select_rows(), recebendo um SQLQuery em vez de sql/params separados."""
return self.select_rows(query.sql, query.params, one=one)
@overload
def select_mappings(self, sql: str, params: dict[str, Any] | None = None, *, one: Literal[True]) -> RowMapping | None: ...
@overload
def select_mappings(self, sql: str, params: dict[str, Any] | None = None, *, one: Literal[False] = False) -> Sequence[RowMapping]: ...
def select_mappings(self, sql: str, params: dict[str, Any] | None = None, *, one: bool = False) -> RowMapping | Sequence[RowMapping] | None:
"""Abre uma conexão e executa um SELECT que retorna dicionários (RowMapping). \n
Parâmetro one indica se deve retornar um Row ou uma Sequence de Rows."""
with self.engine.connect() as conn:
result = conn.execute(text(sql), params).mappings()
return result.fetchone() if one else result.fetchall()
@overload
def select_mappings_query(self, query: SQLQuery, *, one: Literal[True]) -> RowMapping | None: ...
@overload
def select_mappings_query(self, query: SQLQuery, *, one: Literal[False] = False) -> Sequence[RowMapping]: ...
def select_mappings_query(self, query: SQLQuery, *, one: bool = False) -> RowMapping | Sequence[RowMapping] | None:
"""Mesmo que select_mappings(), recebendo um SQLQuery em vez de sql/params separados."""
return self.select_mappings(query.sql, query.params, one=one)
@overload
def execute_returning(self, sql: str, params: dict[str, Any] | None = None, *, one: Literal[True]) -> Row[Any] | None: ...
@overload
def execute_returning(self, sql: str, params: dict[str, Any] | None = None, *, one: Literal[False] = False) -> Sequence[Row[Any]]: ...
def execute_returning(self, sql: str, params: dict[str, Any] | None = None, *, one: bool = False) -> Row[Any] | Sequence[Row[Any]] | None:
"""Abre uma transação, executa e commita um INSERT/UPDATE/DELETE com retorno."""
with self.engine.begin() as conn:
result = conn.execute(text(sql), params)
return result.fetchone() if one else result.fetchall()
@overload
def execute_returning_query(self, query: SQLQuery, *, one: Literal[True]) -> Row[Any] | None: ...
@overload
def execute_returning_query(self, query: SQLQuery, *, one: Literal[False] = False) -> Sequence[Row[Any]]: ...
def execute_returning_query(self, query: SQLQuery, *, one: bool = False) -> Row[Any] | Sequence[Row[Any]] | None:
"""Mesmo que execute_returning(), recebendo um SQLQuery em vez de sql/params separados."""
return self.execute_returning(query.sql, query.params, one=one)
def close(self) -> None:
"""Fecha o pool de conexões do engine."""
self.engine.dispose()
def __enter__(self) -> "DatabaseConnection":
return self
def __exit__(self, *exc_info) -> None:
self.close()

7
database/query.py Normal file
View File

@ -0,0 +1,7 @@
from dataclasses import dataclass, field
from typing import Any
@dataclass(slots=True, frozen=True)
class SQLQuery:
sql: str
params: dict[str, Any] = field(default_factory=dict)

64
database/settings.py Normal file
View File

@ -0,0 +1,64 @@
from pathlib import Path
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import BaseModel, Field, field_validator, ValidationError, SecretStr
from sqlalchemy.engine import URL
BASE_DIR = Path(__file__).resolve().parents[2]
ENV_FILE_PATH = BASE_DIR / ".env"
SUPPORTED_DRIVERS = {
"postgresql": "postgresql+psycopg2",
"mysql": "mysql+pymysql",
"sqlserver": "mssql+pyodbc",
"sqlite": "sqlite"
}
class DatabaseConfig(BaseModel):
user: str
password: SecretStr
host: str
port: int
database: str
driver: str
@field_validator("driver")
def validate_driver(cls, v):
if v not in SUPPORTED_DRIVERS:
raise ValueError(f"Driver '{v}' não suportado. Use um de: {list(SUPPORTED_DRIVERS.keys())}")
return v
def sqlalchemy_url(self) -> URL:
drivername = SUPPORTED_DRIVERS[self.driver]
return URL.create(
drivername=drivername,
username=self.user,
password=self.password.get_secret_value(),
host=self.host,
port=self.port,
database=self.database,
)
class DatabaseSettings(BaseSettings):
database: dict[str, DatabaseConfig]
model_config = SettingsConfigDict(
env_file=ENV_FILE_PATH,
env_file_encoding="utf-8",
env_nested_delimiter="__",
extra="ignore"
)
class Settings:
databases = DatabaseSettings()
def get_url_database(self, database: str) -> URL:
try:
return self.databases.database[database].sqlalchemy_url()
except KeyError as e:
raise ValueError(f"Database '{database}' não encontrado. Use um de: {list(self.databases.database.keys())}")
def get_url_databases(self) -> dict[str, URL]:
return {
name: config.sqlalchemy_url() for name, config in self.databases.database.items()
}

20
manage.py Normal file
View File

@ -0,0 +1,20 @@
#!/usr/bin/env python
"""Utilitário de linha de comando do Django para tarefas administrativas."""
import os
import sys
def main():
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Não foi possível importar o Django. Ele está instalado e disponível na "
"variável de ambiente PYTHONPATH? Você esqueceu de ativar um ambiente virtual?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == "__main__":
main()

View File

@ -0,0 +1 @@
x

View File

@ -0,0 +1 @@
x

Binary file not shown.

View File

@ -0,0 +1 @@
x

View File

@ -0,0 +1 @@
x

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1,67 @@
Cód. Contr/Contrat. Pagador;Id. Título;Número Título;Valor Bruto;Valor Líquido;Situação Título;Contratante;Id. Titular;Família;Id. Benef.;Cód.Beneficiário;Nome Beneficiário;Tipo Benef.;Grau Depend.;Código Combinação;Reservado;Valor;Competência;Tipo;Tipo Ato Médico;Cód. Serviço;Descrição Serviço
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;757515;MARCOS AURELIO FABRIN - 832585;757515;'9750167003394433;MARCOS AURELIO FABRIN;1;0 - TITULAR;10002;;177,85;jul/26;Mensalidade;;;
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;757515;MARCOS AURELIO FABRIN - 832585;751229;'9750167003394450;CRISTINA PEREIRA MORENO FABRIN;2;1 - CONJUGE;10220;;195,82;jul/26;Mensalidade;;;
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;757515;MARCOS AURELIO FABRIN - 832585;751229;'9750167003394450;CRISTINA PEREIRA MORENO FABRIN;2;1 - CONJUGE;10220;;50;jul/26;Serviços ;Consulta;10101012;Consulta em consultorio no horario normal ou preestabelecido
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;757515;MARCOS AURELIO FABRIN - 832585;751229;'9750167003394450;CRISTINA PEREIRA MORENO FABRIN;2;1 - CONJUGE;10220;;50;jul/26;Serviços ;Consulta;10102504;CONSULTA ELETIVA ENDOCRINOLOGIA - CEM FOZ
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;757515;MARCOS AURELIO FABRIN - 832585;751229;'9750167003394450;CRISTINA PEREIRA MORENO FABRIN;2;1 - CONJUGE;10220;;6,19;jul/26;Serviços ;Exame;40310213;Cultura, urina com contagem de colonias
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;757515;MARCOS AURELIO FABRIN - 832585;751229;'9750167003394450;CRISTINA PEREIRA MORENO FABRIN;2;1 - CONJUGE;10220;;2,48;jul/26;Serviços ;Exame;40311210;Rotina de urina caracteres fisicos, elementos anormais e sedimentoscopia
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;757515;MARCOS AURELIO FABRIN - 832585;751229;'9750167003394450;CRISTINA PEREIRA MORENO FABRIN;2;1 - CONJUGE;10220;;8,05;jul/26;Serviços ;Exame;40316289;Foliculo estimulante, hormonio FSH - pesquisa e/ou dosagem
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;757515;MARCOS AURELIO FABRIN - 832585;751229;'9750167003394450;CRISTINA PEREIRA MORENO FABRIN;2;1 - CONJUGE;10220;;7,8;jul/26;Serviços ;Exame;40316335;Hormonio luteinizante LH - pesquisa e/ou dosagem
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;757515;MARCOS AURELIO FABRIN - 832585;751229;'9750167003394450;CRISTINA PEREIRA MORENO FABRIN;2;1 - CONJUGE;10220;;13,62;jul/26;Serviços ;Exame;40316459;Sulfato de dehidroepiandrosterona S-DHEA - pesquisa e/ou dosagem
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;757515;MARCOS AURELIO FABRIN - 832585;751229;'9750167003394450;CRISTINA PEREIRA MORENO FABRIN;2;1 - CONJUGE;10220;;18,58;jul/26;Serviços ;Exame;40316505;Testosterona livre - pesquisa e/ou dosagem
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;757515;MARCOS AURELIO FABRIN - 832585;751229;'9750167003394450;CRISTINA PEREIRA MORENO FABRIN;2;1 - CONJUGE;10220;;13,01;jul/26;Serviços ;Exame;40316513;Testosterona total - pesquisa e/ou dosagem
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;757515;MARCOS AURELIO FABRIN - 832585;751229;'9750167003394450;CRISTINA PEREIRA MORENO FABRIN;2;1 - CONJUGE;10220;;139,54;jul/26;Serviços ;Exame;40601293;Procedimento diagnostico por captura hibrida
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;757515;MARCOS AURELIO FABRIN - 832585;765519;'9750167003780007;EDUARDA VALENTINA FABRIN;2;10 - FILHO(A);10002;;113,9;jul/26;Mensalidade;;;
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;757515;MARCOS AURELIO FABRIN - 832585;765519;'9750167003780007;EDUARDA VALENTINA FABRIN;2;10 - FILHO(A);10002;;1,74;jul/26;Serviços ;Exame;40301630;Creatinina - pesquisa e/ou dosagem
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;757515;MARCOS AURELIO FABRIN - 832585;765519;'9750167003780007;EDUARDA VALENTINA FABRIN;2;10 - FILHO(A);10002;;3,72;jul/26;Serviços ;Exame;40304361;Hemograma com contagem de plaquetas ou fracoes eritrograma, leucograma, plaquet
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;757515;MARCOS AURELIO FABRIN - 832585;765519;'9750167003780007;EDUARDA VALENTINA FABRIN;2;10 - FILHO(A);10002;;6,19;jul/26;Serviços ;Exame;40310213;Cultura, urina com contagem de colonias
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;757515;MARCOS AURELIO FABRIN - 832585;765519;'9750167003780007;EDUARDA VALENTINA FABRIN;2;10 - FILHO(A);10002;;2,48;jul/26;Serviços ;Exame;40310418;Antibiograma teste de sensibilidade e antibioticos e quimioterapicos, por bact
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;757515;MARCOS AURELIO FABRIN - 832585;765519;'9750167003780007;EDUARDA VALENTINA FABRIN;2;10 - FILHO(A);10002;;2,48;jul/26;Serviços ;Exame;40311210;Rotina de urina caracteres fisicos, elementos anormais e sedimentoscopia
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;751302;LEANDRO MARAFON ALVES - 832941;751302;'9750167003423980;LEANDRO MARAFON ALVES;1;0 - TITULAR;10002;;177,85;jul/26;Mensalidade;;;
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;751302;LEANDRO MARAFON ALVES - 832941;751303;'9750167003423999;ALINE RAMOS DO NASCIMENTO MARAFON;2;1 - CONJUGE;10220;;195,82;jul/26;Mensalidade;;;
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;751302;LEANDRO MARAFON ALVES - 832941;762343;'9750167003601226;LORENA DO NASCIMENTO MARAFON ALVES;2;10 - FILHO(A);10002;;113,9;jul/26;Mensalidade;;;
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;751302;LEANDRO MARAFON ALVES - 832941;763663;'9750167003674223;LEONARDO DO NASCIMENTO MARAFON ALVES;2;10 - FILHO(A);10002;;113,9;jul/26;Mensalidade;;;
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;767220;MATHEUS PRESCINOTTI - 836243;767220;'9750167003680177;MATHEUS PRESCINOTTI;1;0 - TITULAR;10002;;159,87;jul/26;Mensalidade;;;
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;759518;MAURICIO FABRIN - 837231;759518;'9750167003742768;MAURICIO FABRIN;1;0 - TITULAR;10002;;143,88;jul/26;Mensalidade;;;
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;759518;MAURICIO FABRIN - 837231;759518;'9750167003742768;MAURICIO FABRIN;1;0 - TITULAR;10002;;50;jul/26;Serviços ;Consulta;10101012;Consulta em consultorio no horario normal ou preestabelecido
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;759518;MAURICIO FABRIN - 837231;759518;'9750167003742768;MAURICIO FABRIN;1;0 - TITULAR;10002;;69,77;jul/26;Serviços ;Exame;40901106;Ecodopplercardiograma transtoracico
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;759518;MAURICIO FABRIN - 837231;763713;'9750167003744671;RUBIA MILENA MIGLIOLI REIS;2;1 - CONJUGE;10220;;144,27;jul/26;Mensalidade;;;
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;759518;MAURICIO FABRIN - 837231;767370;'9750167003795080;ELOAH VITORIA MIGLIOLI FABRIN;2;10 - FILHO(A);10002;;113,9;jul/26;Mensalidade;;;
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;759518;MAURICIO FABRIN - 837231;846589;'9750167018776477;SAMUEL MIGLIOLI FABRIN;2;10 - FILHO(A);10220;;121,89;jul/26;Mensalidade;;;
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;759518;MAURICIO FABRIN - 837231;846589;'9750167018776477;SAMUEL MIGLIOLI FABRIN;2;10 - FILHO(A);10220;;69,77;jul/26;Serviços ;Consulta;10106146;Atendimento ambulatorial em puericultura com diretriz clinica definida pela ANS
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;759987;JEAN RUBEN SOUZA JARA - 837371;759987;'9750167003751279;JEAN RUBEN SOUZA JARA;1;0 - TITULAR;10002;;497,6;jul/26;Mensalidade;;;
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;759987;JEAN RUBEN SOUZA JARA - 837371;759988;'9750167003751287;PATRICIA VALESKA FOTIADIS HENRIQUES JARA;2;1 - CONJUGE;10002;;497,6;jul/26;Mensalidade;;;
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;759987;JEAN RUBEN SOUZA JARA - 837371;760022;'9750167003751295;GIOVANNA FOTIADIS HENRIQUES JARA;2;10 - FILHO(A);10002;;113,9;jul/26;Mensalidade;;;
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;762851;VALDECIR PROVIN - 837851;762851;'9750167003776174;VALDECIR PROVIN;1;0 - TITULAR;10002;;621,51;jul/26;Mensalidade;;;
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;762851;VALDECIR PROVIN - 837851;762852;'9750167003776182;SUZANA DE ALMEIDA;2;1 - CONJUGE;10002;;283,78;jul/26;Mensalidade;;;
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;762851;VALDECIR PROVIN - 837851;762852;'9750167003776182;SUZANA DE ALMEIDA;2;1 - CONJUGE;10002;;1,74;jul/26;Serviços ;Exame;40301060;Acido ascorbico vitamina C - pesquisa e/ou dosagem
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;762851;VALDECIR PROVIN - 837851;762852;'9750167003776182;SUZANA DE ALMEIDA;2;1 - CONJUGE;10002;;3,72;jul/26;Serviços ;Exame;40301583;Colesterol HDL - pesquisa e/ou dosagem
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;762851;VALDECIR PROVIN - 837851;762852;'9750167003776182;SUZANA DE ALMEIDA;2;1 - CONJUGE;10002;;4,96;jul/26;Serviços ;Exame;40301591;Colesterol LDL - pesquisa e/ou dosagem
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;762851;VALDECIR PROVIN - 837851;762852;'9750167003776182;SUZANA DE ALMEIDA;2;1 - CONJUGE;10002;;1,74;jul/26;Serviços ;Exame;40301605;Colesterol total - pesquisa e/ou dosagem
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;762851;VALDECIR PROVIN - 837851;762852;'9750167003776182;SUZANA DE ALMEIDA;2;1 - CONJUGE;10002;;2,23;jul/26;Serviços ;Exame;40301842;Ferro serico - pesquisa e/ou dosagem
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;762851;VALDECIR PROVIN - 837851;762852;'9750167003776182;SUZANA DE ALMEIDA;2;1 - CONJUGE;10002;;1,74;jul/26;Serviços ;Exame;40302040;Glicose - pesquisa e/ou dosagem
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;762851;VALDECIR PROVIN - 837851;762852;'9750167003776182;SUZANA DE ALMEIDA;2;1 - CONJUGE;10002;;8,4;jul/26;Serviços ;Exame;40302075;Hemoglobina glicada A1 total - pesquisa e/ou dosagem
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;762851;VALDECIR PROVIN - 837851;762852;'9750167003776182;SUZANA DE ALMEIDA;2;1 - CONJUGE;10002;;1,98;jul/26;Serviços ;Exame;40302237;Magnesio - pesquisa e/ou dosagem
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;762851;VALDECIR PROVIN - 837851;762852;'9750167003776182;SUZANA DE ALMEIDA;2;1 - CONJUGE;10002;;8,28;jul/26;Serviços ;Exame;40302830;Vitamina D 25 HIDROXI, pesquisa e/ou dosagem Vitamina D3
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;762851;VALDECIR PROVIN - 837851;762852;'9750167003776182;SUZANA DE ALMEIDA;2;1 - CONJUGE;10002;;3,72;jul/26;Serviços ;Exame;40304361;Hemograma com contagem de plaquetas ou fracoes eritrograma, leucograma, plaquet
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;762851;VALDECIR PROVIN - 837851;762852;'9750167003776182;SUZANA DE ALMEIDA;2;1 - CONJUGE;10002;;6,19;jul/26;Serviços ;Exame;40310213;Cultura, urina com contagem de colonias
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;762851;VALDECIR PROVIN - 837851;762852;'9750167003776182;SUZANA DE ALMEIDA;2;1 - CONJUGE;10002;;2,48;jul/26;Serviços ;Exame;40311210;Rotina de urina caracteres fisicos, elementos anormais e sedimentoscopia
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;762851;VALDECIR PROVIN - 837851;762852;'9750167003776182;SUZANA DE ALMEIDA;2;1 - CONJUGE;10002;;14,86;jul/26;Serviços ;Exame;40313328;Zinco - pesquisa e/ou dosagem
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;762851;VALDECIR PROVIN - 837851;762852;'9750167003776182;SUZANA DE ALMEIDA;2;1 - CONJUGE;10002;;19,82;jul/26;Serviços ;Exame;40316076;Androstenediona - pesquisa e/ou dosagem
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;762851;VALDECIR PROVIN - 837851;762852;'9750167003776182;SUZANA DE ALMEIDA;2;1 - CONJUGE;10002;;9,29;jul/26;Serviços ;Exame;40316190;Cortisol - pesquisa e/ou dosagem
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;762851;VALDECIR PROVIN - 837851;762852;'9750167003776182;SUZANA DE ALMEIDA;2;1 - CONJUGE;10002;;12,39;jul/26;Serviços ;Exame;40316246;Estradiol - pesquisa e/ou dosagem
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;762851;VALDECIR PROVIN - 837851;762852;'9750167003776182;SUZANA DE ALMEIDA;2;1 - CONJUGE;10002;;15,48;jul/26;Serviços ;Exame;40316270;Ferritina - pesquisa e/ou dosagem
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;762851;VALDECIR PROVIN - 837851;762852;'9750167003776182;SUZANA DE ALMEIDA;2;1 - CONJUGE;10002;;8,05;jul/26;Serviços ;Exame;40316289;Foliculo estimulante, hormonio FSH - pesquisa e/ou dosagem
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;762851;VALDECIR PROVIN - 837851;762852;'9750167003776182;SUZANA DE ALMEIDA;2;1 - CONJUGE;10002;;7,8;jul/26;Serviços ;Exame;40316335;Hormonio luteinizante LH - pesquisa e/ou dosagem
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;762851;VALDECIR PROVIN - 837851;762852;'9750167003776182;SUZANA DE ALMEIDA;2;1 - CONJUGE;10002;;6,81;jul/26;Serviços ;Exame;40316360;Insulina - pesquisa e/ou dosagem
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;762851;VALDECIR PROVIN - 837851;762852;'9750167003776182;SUZANA DE ALMEIDA;2;1 - CONJUGE;10002;;10,53;jul/26;Serviços ;Exame;40316416;Prolactina - pesquisa e/ou dosagem
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;762851;VALDECIR PROVIN - 837851;762852;'9750167003776182;SUZANA DE ALMEIDA;2;1 - CONJUGE;10002;;10,53;jul/26;Serviços ;Exame;40316491;T4 livre - pesquisa e/ou dosagem
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;762851;VALDECIR PROVIN - 837851;762852;'9750167003776182;SUZANA DE ALMEIDA;2;1 - CONJUGE;10002;;18,58;jul/26;Serviços ;Exame;40316505;Testosterona livre - pesquisa e/ou dosagem
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;762851;VALDECIR PROVIN - 837851;762852;'9750167003776182;SUZANA DE ALMEIDA;2;1 - CONJUGE;10002;;10,53;jul/26;Serviços ;Exame;40316521;Tireoestimulante, hormonio TSH - pesquisa e/ou dosagem
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;762851;VALDECIR PROVIN - 837851;762852;'9750167003776182;SUZANA DE ALMEIDA;2;1 - CONJUGE;10002;;10,53;jul/26;Serviços ;Exame;40316572;Vitamina B12 - pesquisa e/ou dosagem
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;762851;VALDECIR PROVIN - 837851;762853;'9750167003776190;MAURICIO PROVIN NETO;2;10 - FILHO(A);10002;;143,88;jul/26;Mensalidade;;;
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;762851;VALDECIR PROVIN - 837851;762853;'9750167003776190;MAURICIO PROVIN NETO;2;10 - FILHO(A);10002;;69,77;jul/26;Serviços ;Consulta;10101039;Consulta em pronto socorro
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;764799;ENALTO FERREIRA MIRANDA - 837889;764799;'9750167003777987;ENALTO FERREIRA MIRANDA;1;0 - TITULAR;10002;;683,47;jul/26;Mensalidade;;;
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;764799;ENALTO FERREIRA MIRANDA - 837889;764799;'9750167003777987;ENALTO FERREIRA MIRANDA;1;0 - TITULAR;10002;;50;jul/26;Serviços ;Consulta;10101012;Consulta em consultorio no horario normal ou preestabelecido
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;764799;ENALTO FERREIRA MIRANDA - 837889;764800;'9750167003777995;ELAINE LIMA MIRANDA;2;10 - FILHO(A);10220;;195,82;jul/26;Mensalidade;;;
5880360-0;3151518;201924969;5807,85;5807,85;Aberto;PRESCINOTTI & CIA LTDA - 77307387000158;764799;ENALTO FERREIRA MIRANDA - 837889;766687;'9750167003804380;EDERSON LIMA MIRANDA;2;10 - FILHO(A);10002;;159,87;jul/26;Mensalidade;;;
1 Cód. Contr/Contrat. Pagador Id. Título Número Título Valor Bruto Valor Líquido Situação Título Contratante Id. Titular Família Id. Benef. Cód.Beneficiário Nome Beneficiário Tipo Benef. Grau Depend. Código Combinação Reservado Valor Competência Tipo Tipo Ato Médico Cód. Serviço Descrição Serviço
2 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 757515 MARCOS AURELIO FABRIN - 832585 757515 '9750167003394433 MARCOS AURELIO FABRIN 1 0 - TITULAR 10002 177,85 jul/26 Mensalidade
3 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 757515 MARCOS AURELIO FABRIN - 832585 751229 '9750167003394450 CRISTINA PEREIRA MORENO FABRIN 2 1 - CONJUGE 10220 195,82 jul/26 Mensalidade
4 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 757515 MARCOS AURELIO FABRIN - 832585 751229 '9750167003394450 CRISTINA PEREIRA MORENO FABRIN 2 1 - CONJUGE 10220 50 jul/26 Serviços Consulta 10101012 Consulta em consultorio no horario normal ou preestabelecido
5 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 757515 MARCOS AURELIO FABRIN - 832585 751229 '9750167003394450 CRISTINA PEREIRA MORENO FABRIN 2 1 - CONJUGE 10220 50 jul/26 Serviços Consulta 10102504 CONSULTA ELETIVA ENDOCRINOLOGIA - CEM FOZ
6 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 757515 MARCOS AURELIO FABRIN - 832585 751229 '9750167003394450 CRISTINA PEREIRA MORENO FABRIN 2 1 - CONJUGE 10220 6,19 jul/26 Serviços Exame 40310213 Cultura, urina com contagem de colonias
7 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 757515 MARCOS AURELIO FABRIN - 832585 751229 '9750167003394450 CRISTINA PEREIRA MORENO FABRIN 2 1 - CONJUGE 10220 2,48 jul/26 Serviços Exame 40311210 Rotina de urina caracteres fisicos, elementos anormais e sedimentoscopia
8 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 757515 MARCOS AURELIO FABRIN - 832585 751229 '9750167003394450 CRISTINA PEREIRA MORENO FABRIN 2 1 - CONJUGE 10220 8,05 jul/26 Serviços Exame 40316289 Foliculo estimulante, hormonio FSH - pesquisa e/ou dosagem
9 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 757515 MARCOS AURELIO FABRIN - 832585 751229 '9750167003394450 CRISTINA PEREIRA MORENO FABRIN 2 1 - CONJUGE 10220 7,8 jul/26 Serviços Exame 40316335 Hormonio luteinizante LH - pesquisa e/ou dosagem
10 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 757515 MARCOS AURELIO FABRIN - 832585 751229 '9750167003394450 CRISTINA PEREIRA MORENO FABRIN 2 1 - CONJUGE 10220 13,62 jul/26 Serviços Exame 40316459 Sulfato de dehidroepiandrosterona S-DHEA - pesquisa e/ou dosagem
11 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 757515 MARCOS AURELIO FABRIN - 832585 751229 '9750167003394450 CRISTINA PEREIRA MORENO FABRIN 2 1 - CONJUGE 10220 18,58 jul/26 Serviços Exame 40316505 Testosterona livre - pesquisa e/ou dosagem
12 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 757515 MARCOS AURELIO FABRIN - 832585 751229 '9750167003394450 CRISTINA PEREIRA MORENO FABRIN 2 1 - CONJUGE 10220 13,01 jul/26 Serviços Exame 40316513 Testosterona total - pesquisa e/ou dosagem
13 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 757515 MARCOS AURELIO FABRIN - 832585 751229 '9750167003394450 CRISTINA PEREIRA MORENO FABRIN 2 1 - CONJUGE 10220 139,54 jul/26 Serviços Exame 40601293 Procedimento diagnostico por captura hibrida
14 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 757515 MARCOS AURELIO FABRIN - 832585 765519 '9750167003780007 EDUARDA VALENTINA FABRIN 2 10 - FILHO(A) 10002 113,9 jul/26 Mensalidade
15 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 757515 MARCOS AURELIO FABRIN - 832585 765519 '9750167003780007 EDUARDA VALENTINA FABRIN 2 10 - FILHO(A) 10002 1,74 jul/26 Serviços Exame 40301630 Creatinina - pesquisa e/ou dosagem
16 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 757515 MARCOS AURELIO FABRIN - 832585 765519 '9750167003780007 EDUARDA VALENTINA FABRIN 2 10 - FILHO(A) 10002 3,72 jul/26 Serviços Exame 40304361 Hemograma com contagem de plaquetas ou fracoes eritrograma, leucograma, plaquet
17 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 757515 MARCOS AURELIO FABRIN - 832585 765519 '9750167003780007 EDUARDA VALENTINA FABRIN 2 10 - FILHO(A) 10002 6,19 jul/26 Serviços Exame 40310213 Cultura, urina com contagem de colonias
18 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 757515 MARCOS AURELIO FABRIN - 832585 765519 '9750167003780007 EDUARDA VALENTINA FABRIN 2 10 - FILHO(A) 10002 2,48 jul/26 Serviços Exame 40310418 Antibiograma teste de sensibilidade e antibioticos e quimioterapicos, por bact
19 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 757515 MARCOS AURELIO FABRIN - 832585 765519 '9750167003780007 EDUARDA VALENTINA FABRIN 2 10 - FILHO(A) 10002 2,48 jul/26 Serviços Exame 40311210 Rotina de urina caracteres fisicos, elementos anormais e sedimentoscopia
20 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 751302 LEANDRO MARAFON ALVES - 832941 751302 '9750167003423980 LEANDRO MARAFON ALVES 1 0 - TITULAR 10002 177,85 jul/26 Mensalidade
21 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 751302 LEANDRO MARAFON ALVES - 832941 751303 '9750167003423999 ALINE RAMOS DO NASCIMENTO MARAFON 2 1 - CONJUGE 10220 195,82 jul/26 Mensalidade
22 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 751302 LEANDRO MARAFON ALVES - 832941 762343 '9750167003601226 LORENA DO NASCIMENTO MARAFON ALVES 2 10 - FILHO(A) 10002 113,9 jul/26 Mensalidade
23 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 751302 LEANDRO MARAFON ALVES - 832941 763663 '9750167003674223 LEONARDO DO NASCIMENTO MARAFON ALVES 2 10 - FILHO(A) 10002 113,9 jul/26 Mensalidade
24 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 767220 MATHEUS PRESCINOTTI - 836243 767220 '9750167003680177 MATHEUS PRESCINOTTI 1 0 - TITULAR 10002 159,87 jul/26 Mensalidade
25 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 759518 MAURICIO FABRIN - 837231 759518 '9750167003742768 MAURICIO FABRIN 1 0 - TITULAR 10002 143,88 jul/26 Mensalidade
26 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 759518 MAURICIO FABRIN - 837231 759518 '9750167003742768 MAURICIO FABRIN 1 0 - TITULAR 10002 50 jul/26 Serviços Consulta 10101012 Consulta em consultorio no horario normal ou preestabelecido
27 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 759518 MAURICIO FABRIN - 837231 759518 '9750167003742768 MAURICIO FABRIN 1 0 - TITULAR 10002 69,77 jul/26 Serviços Exame 40901106 Ecodopplercardiograma transtoracico
28 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 759518 MAURICIO FABRIN - 837231 763713 '9750167003744671 RUBIA MILENA MIGLIOLI REIS 2 1 - CONJUGE 10220 144,27 jul/26 Mensalidade
29 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 759518 MAURICIO FABRIN - 837231 767370 '9750167003795080 ELOAH VITORIA MIGLIOLI FABRIN 2 10 - FILHO(A) 10002 113,9 jul/26 Mensalidade
30 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 759518 MAURICIO FABRIN - 837231 846589 '9750167018776477 SAMUEL MIGLIOLI FABRIN 2 10 - FILHO(A) 10220 121,89 jul/26 Mensalidade
31 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 759518 MAURICIO FABRIN - 837231 846589 '9750167018776477 SAMUEL MIGLIOLI FABRIN 2 10 - FILHO(A) 10220 69,77 jul/26 Serviços Consulta 10106146 Atendimento ambulatorial em puericultura com diretriz clinica definida pela ANS
32 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 759987 JEAN RUBEN SOUZA JARA - 837371 759987 '9750167003751279 JEAN RUBEN SOUZA JARA 1 0 - TITULAR 10002 497,6 jul/26 Mensalidade
33 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 759987 JEAN RUBEN SOUZA JARA - 837371 759988 '9750167003751287 PATRICIA VALESKA FOTIADIS HENRIQUES JARA 2 1 - CONJUGE 10002 497,6 jul/26 Mensalidade
34 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 759987 JEAN RUBEN SOUZA JARA - 837371 760022 '9750167003751295 GIOVANNA FOTIADIS HENRIQUES JARA 2 10 - FILHO(A) 10002 113,9 jul/26 Mensalidade
35 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 762851 VALDECIR PROVIN - 837851 762851 '9750167003776174 VALDECIR PROVIN 1 0 - TITULAR 10002 621,51 jul/26 Mensalidade
36 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 762851 VALDECIR PROVIN - 837851 762852 '9750167003776182 SUZANA DE ALMEIDA 2 1 - CONJUGE 10002 283,78 jul/26 Mensalidade
37 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 762851 VALDECIR PROVIN - 837851 762852 '9750167003776182 SUZANA DE ALMEIDA 2 1 - CONJUGE 10002 1,74 jul/26 Serviços Exame 40301060 Acido ascorbico vitamina C - pesquisa e/ou dosagem
38 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 762851 VALDECIR PROVIN - 837851 762852 '9750167003776182 SUZANA DE ALMEIDA 2 1 - CONJUGE 10002 3,72 jul/26 Serviços Exame 40301583 Colesterol HDL - pesquisa e/ou dosagem
39 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 762851 VALDECIR PROVIN - 837851 762852 '9750167003776182 SUZANA DE ALMEIDA 2 1 - CONJUGE 10002 4,96 jul/26 Serviços Exame 40301591 Colesterol LDL - pesquisa e/ou dosagem
40 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 762851 VALDECIR PROVIN - 837851 762852 '9750167003776182 SUZANA DE ALMEIDA 2 1 - CONJUGE 10002 1,74 jul/26 Serviços Exame 40301605 Colesterol total - pesquisa e/ou dosagem
41 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 762851 VALDECIR PROVIN - 837851 762852 '9750167003776182 SUZANA DE ALMEIDA 2 1 - CONJUGE 10002 2,23 jul/26 Serviços Exame 40301842 Ferro serico - pesquisa e/ou dosagem
42 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 762851 VALDECIR PROVIN - 837851 762852 '9750167003776182 SUZANA DE ALMEIDA 2 1 - CONJUGE 10002 1,74 jul/26 Serviços Exame 40302040 Glicose - pesquisa e/ou dosagem
43 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 762851 VALDECIR PROVIN - 837851 762852 '9750167003776182 SUZANA DE ALMEIDA 2 1 - CONJUGE 10002 8,4 jul/26 Serviços Exame 40302075 Hemoglobina glicada A1 total - pesquisa e/ou dosagem
44 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 762851 VALDECIR PROVIN - 837851 762852 '9750167003776182 SUZANA DE ALMEIDA 2 1 - CONJUGE 10002 1,98 jul/26 Serviços Exame 40302237 Magnesio - pesquisa e/ou dosagem
45 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 762851 VALDECIR PROVIN - 837851 762852 '9750167003776182 SUZANA DE ALMEIDA 2 1 - CONJUGE 10002 8,28 jul/26 Serviços Exame 40302830 Vitamina D 25 HIDROXI, pesquisa e/ou dosagem Vitamina D3
46 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 762851 VALDECIR PROVIN - 837851 762852 '9750167003776182 SUZANA DE ALMEIDA 2 1 - CONJUGE 10002 3,72 jul/26 Serviços Exame 40304361 Hemograma com contagem de plaquetas ou fracoes eritrograma, leucograma, plaquet
47 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 762851 VALDECIR PROVIN - 837851 762852 '9750167003776182 SUZANA DE ALMEIDA 2 1 - CONJUGE 10002 6,19 jul/26 Serviços Exame 40310213 Cultura, urina com contagem de colonias
48 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 762851 VALDECIR PROVIN - 837851 762852 '9750167003776182 SUZANA DE ALMEIDA 2 1 - CONJUGE 10002 2,48 jul/26 Serviços Exame 40311210 Rotina de urina caracteres fisicos, elementos anormais e sedimentoscopia
49 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 762851 VALDECIR PROVIN - 837851 762852 '9750167003776182 SUZANA DE ALMEIDA 2 1 - CONJUGE 10002 14,86 jul/26 Serviços Exame 40313328 Zinco - pesquisa e/ou dosagem
50 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 762851 VALDECIR PROVIN - 837851 762852 '9750167003776182 SUZANA DE ALMEIDA 2 1 - CONJUGE 10002 19,82 jul/26 Serviços Exame 40316076 Androstenediona - pesquisa e/ou dosagem
51 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 762851 VALDECIR PROVIN - 837851 762852 '9750167003776182 SUZANA DE ALMEIDA 2 1 - CONJUGE 10002 9,29 jul/26 Serviços Exame 40316190 Cortisol - pesquisa e/ou dosagem
52 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 762851 VALDECIR PROVIN - 837851 762852 '9750167003776182 SUZANA DE ALMEIDA 2 1 - CONJUGE 10002 12,39 jul/26 Serviços Exame 40316246 Estradiol - pesquisa e/ou dosagem
53 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 762851 VALDECIR PROVIN - 837851 762852 '9750167003776182 SUZANA DE ALMEIDA 2 1 - CONJUGE 10002 15,48 jul/26 Serviços Exame 40316270 Ferritina - pesquisa e/ou dosagem
54 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 762851 VALDECIR PROVIN - 837851 762852 '9750167003776182 SUZANA DE ALMEIDA 2 1 - CONJUGE 10002 8,05 jul/26 Serviços Exame 40316289 Foliculo estimulante, hormonio FSH - pesquisa e/ou dosagem
55 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 762851 VALDECIR PROVIN - 837851 762852 '9750167003776182 SUZANA DE ALMEIDA 2 1 - CONJUGE 10002 7,8 jul/26 Serviços Exame 40316335 Hormonio luteinizante LH - pesquisa e/ou dosagem
56 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 762851 VALDECIR PROVIN - 837851 762852 '9750167003776182 SUZANA DE ALMEIDA 2 1 - CONJUGE 10002 6,81 jul/26 Serviços Exame 40316360 Insulina - pesquisa e/ou dosagem
57 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 762851 VALDECIR PROVIN - 837851 762852 '9750167003776182 SUZANA DE ALMEIDA 2 1 - CONJUGE 10002 10,53 jul/26 Serviços Exame 40316416 Prolactina - pesquisa e/ou dosagem
58 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 762851 VALDECIR PROVIN - 837851 762852 '9750167003776182 SUZANA DE ALMEIDA 2 1 - CONJUGE 10002 10,53 jul/26 Serviços Exame 40316491 T4 livre - pesquisa e/ou dosagem
59 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 762851 VALDECIR PROVIN - 837851 762852 '9750167003776182 SUZANA DE ALMEIDA 2 1 - CONJUGE 10002 18,58 jul/26 Serviços Exame 40316505 Testosterona livre - pesquisa e/ou dosagem
60 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 762851 VALDECIR PROVIN - 837851 762852 '9750167003776182 SUZANA DE ALMEIDA 2 1 - CONJUGE 10002 10,53 jul/26 Serviços Exame 40316521 Tireoestimulante, hormonio TSH - pesquisa e/ou dosagem
61 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 762851 VALDECIR PROVIN - 837851 762852 '9750167003776182 SUZANA DE ALMEIDA 2 1 - CONJUGE 10002 10,53 jul/26 Serviços Exame 40316572 Vitamina B12 - pesquisa e/ou dosagem
62 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 762851 VALDECIR PROVIN - 837851 762853 '9750167003776190 MAURICIO PROVIN NETO 2 10 - FILHO(A) 10002 143,88 jul/26 Mensalidade
63 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 762851 VALDECIR PROVIN - 837851 762853 '9750167003776190 MAURICIO PROVIN NETO 2 10 - FILHO(A) 10002 69,77 jul/26 Serviços Consulta 10101039 Consulta em pronto socorro
64 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 764799 ENALTO FERREIRA MIRANDA - 837889 764799 '9750167003777987 ENALTO FERREIRA MIRANDA 1 0 - TITULAR 10002 683,47 jul/26 Mensalidade
65 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 764799 ENALTO FERREIRA MIRANDA - 837889 764799 '9750167003777987 ENALTO FERREIRA MIRANDA 1 0 - TITULAR 10002 50 jul/26 Serviços Consulta 10101012 Consulta em consultorio no horario normal ou preestabelecido
66 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 764799 ENALTO FERREIRA MIRANDA - 837889 764800 '9750167003777995 ELAINE LIMA MIRANDA 2 10 - FILHO(A) 10220 195,82 jul/26 Mensalidade
67 5880360-0 3151518 201924969 5807,85 5807,85 Aberto PRESCINOTTI & CIA LTDA - 77307387000158 764799 ENALTO FERREIRA MIRANDA - 837889 766687 '9750167003804380 EDERSON LIMA MIRANDA 2 10 - FILHO(A) 10002 159,87 jul/26 Mensalidade

View File

@ -0,0 +1,82 @@
Cód. Contr/Contrat. Pagador;Id. Título;Número Título;Valor Bruto;Valor Líquido;Situação Título;Contratante;Id. Titular;Família;Id. Benef.;Cód.Beneficiário;Nome Beneficiário;Tipo Benef.;Grau Depend.;Código Combinação;Reservado;Valor;Competência;Tipo;Tipo Ato Médico;Cód. Serviço;Descrição Serviço
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765929;TATIANE WELTER MACHADO DE CARVALHO - 838206;765929;'9750167003795578;TATIANE WELTER MACHADO DE CARVALHO;1;0 - TITULAR;10080;;318,03;07/2026;Mensalidade;;;
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765929;TATIANE WELTER MACHADO DE CARVALHO - 838206;765929;'9750167003795578;TATIANE WELTER MACHADO DE CARVALHO;1;0 - TITULAR;10080;;1,74;07/2026;Serviços ;Exame;40301150;Acido urico - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765929;TATIANE WELTER MACHADO DE CARVALHO - 838206;765929;'9750167003795578;TATIANE WELTER MACHADO DE CARVALHO;1;0 - TITULAR;10080;;1,74;07/2026;Serviços ;Exame;40301400;Calcio - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765929;TATIANE WELTER MACHADO DE CARVALHO - 838206;765929;'9750167003795578;TATIANE WELTER MACHADO DE CARVALHO;1;0 - TITULAR;10080;;14,86;07/2026;Serviços ;Exame;40301567;Cobre - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765929;TATIANE WELTER MACHADO DE CARVALHO - 838206;765929;'9750167003795578;TATIANE WELTER MACHADO DE CARVALHO;1;0 - TITULAR;10080;;3,72;07/2026;Serviços ;Exame;40301583;Colesterol HDL - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765929;TATIANE WELTER MACHADO DE CARVALHO - 838206;765929;'9750167003795578;TATIANE WELTER MACHADO DE CARVALHO;1;0 - TITULAR;10080;;4,96;07/2026;Serviços ;Exame;40301591;Colesterol LDL - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765929;TATIANE WELTER MACHADO DE CARVALHO - 838206;765929;'9750167003795578;TATIANE WELTER MACHADO DE CARVALHO;1;0 - TITULAR;10080;;1,74;07/2026;Serviços ;Exame;40301630;Creatinina - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765929;TATIANE WELTER MACHADO DE CARVALHO - 838206;765929;'9750167003795578;TATIANE WELTER MACHADO DE CARVALHO;1;0 - TITULAR;10080;;2,23;07/2026;Serviços ;Exame;40301842;Ferro serico - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765929;TATIANE WELTER MACHADO DE CARVALHO - 838206;765929;'9750167003795578;TATIANE WELTER MACHADO DE CARVALHO;1;0 - TITULAR;10080;;1,74;07/2026;Serviços ;Exame;40302040;Glicose - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765929;TATIANE WELTER MACHADO DE CARVALHO - 838206;765929;'9750167003795578;TATIANE WELTER MACHADO DE CARVALHO;1;0 - TITULAR;10080;;8,4;07/2026;Serviços ;Exame;40302075;Hemoglobina glicada A1 total - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765929;TATIANE WELTER MACHADO DE CARVALHO - 838206;765929;'9750167003795578;TATIANE WELTER MACHADO DE CARVALHO;1;0 - TITULAR;10080;;1,98;07/2026;Serviços ;Exame;40302237;Magnesio - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765929;TATIANE WELTER MACHADO DE CARVALHO - 838206;765929;'9750167003795578;TATIANE WELTER MACHADO DE CARVALHO;1;0 - TITULAR;10080;;1,74;07/2026;Serviços ;Exame;40302504;Transaminase oxalacetica amino transferase aspartato - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765929;TATIANE WELTER MACHADO DE CARVALHO - 838206;765929;'9750167003795578;TATIANE WELTER MACHADO DE CARVALHO;1;0 - TITULAR;10080;;1,74;07/2026;Serviços ;Exame;40302512;Transaminase piruvica amino transferase de alanina - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765929;TATIANE WELTER MACHADO DE CARVALHO - 838206;765929;'9750167003795578;TATIANE WELTER MACHADO DE CARVALHO;1;0 - TITULAR;10080;;2,48;07/2026;Serviços ;Exame;40302547;Triglicerideos - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765929;TATIANE WELTER MACHADO DE CARVALHO - 838206;765929;'9750167003795578;TATIANE WELTER MACHADO DE CARVALHO;1;0 - TITULAR;10080;;1,74;07/2026;Serviços ;Exame;40302580;Ureia - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765929;TATIANE WELTER MACHADO DE CARVALHO - 838206;765929;'9750167003795578;TATIANE WELTER MACHADO DE CARVALHO;1;0 - TITULAR;10080;;8,8;07/2026;Serviços ;Exame;40302601;Vitamina A, pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765929;TATIANE WELTER MACHADO DE CARVALHO - 838206;765929;'9750167003795578;TATIANE WELTER MACHADO DE CARVALHO;1;0 - TITULAR;10080;;8,28;07/2026;Serviços ;Exame;40302830;Vitamina D 25 HIDROXI, pesquisa e/ou dosagem Vitamina D3
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765929;TATIANE WELTER MACHADO DE CARVALHO - 838206;765929;'9750167003795578;TATIANE WELTER MACHADO DE CARVALHO;1;0 - TITULAR;10080;;3,72;07/2026;Serviços ;Exame;40304361;Hemograma com contagem de plaquetas ou fracoes eritrograma, leucograma, plaquet
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765929;TATIANE WELTER MACHADO DE CARVALHO - 838206;765929;'9750167003795578;TATIANE WELTER MACHADO DE CARVALHO;1;0 - TITULAR;10080;;24,77;07/2026;Serviços ;Exame;40305465;Paratormonio - PTH ou fracao cada - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765929;TATIANE WELTER MACHADO DE CARVALHO - 838206;765929;'9750167003795578;TATIANE WELTER MACHADO DE CARVALHO;1;0 - TITULAR;10080;;14,86;07/2026;Serviços ;Exame;40313328;Zinco - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765929;TATIANE WELTER MACHADO DE CARVALHO - 838206;765929;'9750167003795578;TATIANE WELTER MACHADO DE CARVALHO;1;0 - TITULAR;10080;;9,29;07/2026;Serviços ;Exame;40316190;Cortisol - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765929;TATIANE WELTER MACHADO DE CARVALHO - 838206;765929;'9750167003795578;TATIANE WELTER MACHADO DE CARVALHO;1;0 - TITULAR;10080;;12,39;07/2026;Serviços ;Exame;40316246;Estradiol - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765929;TATIANE WELTER MACHADO DE CARVALHO - 838206;765929;'9750167003795578;TATIANE WELTER MACHADO DE CARVALHO;1;0 - TITULAR;10080;;15,48;07/2026;Serviços ;Exame;40316270;Ferritina - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765929;TATIANE WELTER MACHADO DE CARVALHO - 838206;765929;'9750167003795578;TATIANE WELTER MACHADO DE CARVALHO;1;0 - TITULAR;10080;;8,05;07/2026;Serviços ;Exame;40316289;Foliculo estimulante, hormonio FSH - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765929;TATIANE WELTER MACHADO DE CARVALHO - 838206;765929;'9750167003795578;TATIANE WELTER MACHADO DE CARVALHO;1;0 - TITULAR;10080;;25,76;07/2026;Serviços ;Exame;40316300;Globulina de ligacao de hormonios sexuais SHBG - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765929;TATIANE WELTER MACHADO DE CARVALHO - 838206;765929;'9750167003795578;TATIANE WELTER MACHADO DE CARVALHO;1;0 - TITULAR;10080;;7,8;07/2026;Serviços ;Exame;40316335;Hormonio luteinizante LH - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765929;TATIANE WELTER MACHADO DE CARVALHO - 838206;765929;'9750167003795578;TATIANE WELTER MACHADO DE CARVALHO;1;0 - TITULAR;10080;;6,81;07/2026;Serviços ;Exame;40316360;Insulina - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765929;TATIANE WELTER MACHADO DE CARVALHO - 838206;765929;'9750167003795578;TATIANE WELTER MACHADO DE CARVALHO;1;0 - TITULAR;10080;;13,01;07/2026;Serviços ;Exame;40316408;Progesterona - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765929;TATIANE WELTER MACHADO DE CARVALHO - 838206;765929;'9750167003795578;TATIANE WELTER MACHADO DE CARVALHO;1;0 - TITULAR;10080;;10,53;07/2026;Serviços ;Exame;40316416;Prolactina - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765929;TATIANE WELTER MACHADO DE CARVALHO - 838206;765929;'9750167003795578;TATIANE WELTER MACHADO DE CARVALHO;1;0 - TITULAR;10080;;10,53;07/2026;Serviços ;Exame;40316491;T4 livre - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765929;TATIANE WELTER MACHADO DE CARVALHO - 838206;765929;'9750167003795578;TATIANE WELTER MACHADO DE CARVALHO;1;0 - TITULAR;10080;;18,58;07/2026;Serviços ;Exame;40316505;Testosterona livre - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765929;TATIANE WELTER MACHADO DE CARVALHO - 838206;765929;'9750167003795578;TATIANE WELTER MACHADO DE CARVALHO;1;0 - TITULAR;10080;;13,01;07/2026;Serviços ;Exame;40316513;Testosterona total - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765929;TATIANE WELTER MACHADO DE CARVALHO - 838206;765929;'9750167003795578;TATIANE WELTER MACHADO DE CARVALHO;1;0 - TITULAR;10080;;10,53;07/2026;Serviços ;Exame;40316521;Tireoestimulante, hormonio TSH - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765929;TATIANE WELTER MACHADO DE CARVALHO - 838206;765929;'9750167003795578;TATIANE WELTER MACHADO DE CARVALHO;1;0 - TITULAR;10080;;8,05;07/2026;Serviços ;Exame;40316556;Triiodotironina T3 - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765929;TATIANE WELTER MACHADO DE CARVALHO - 838206;765929;'9750167003795578;TATIANE WELTER MACHADO DE CARVALHO;1;0 - TITULAR;10080;;10,53;07/2026;Serviços ;Exame;40316572;Vitamina B12 - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765929;TATIANE WELTER MACHADO DE CARVALHO - 838206;765911;'9750167003795594;ALEXANDRE DE CARVALHO;2;1 - CONJUGE;10080;;228,72;07/2026;Mensalidade;;;
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765929;TATIANE WELTER MACHADO DE CARVALHO - 838206;765914;'9750167003795624;HELOISA MACHADO DE CARVALHO;2;10 - FILHO(A);10080;;129,61;07/2026;Mensalidade;;;
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765930;LAERCIO WELTER MACHADO - 838207;765930;'9750167003795586;LAERCIO WELTER MACHADO;1;0 - TITULAR;10080;;541,31;07/2026;Mensalidade;;;
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765930;LAERCIO WELTER MACHADO - 838207;765930;'9750167003795586;LAERCIO WELTER MACHADO;1;0 - TITULAR;10080;;1,74;07/2026;Serviços ;Exame;40301060;Acido ascorbico vitamina C - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765930;LAERCIO WELTER MACHADO - 838207;765930;'9750167003795586;LAERCIO WELTER MACHADO;1;0 - TITULAR;10080;;10,53;07/2026;Serviços ;Exame;40301087;Acido folico, pesquisa e/ou dosagem nos eritrocitos
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765930;LAERCIO WELTER MACHADO - 838207;765930;'9750167003795586;LAERCIO WELTER MACHADO;1;0 - TITULAR;10080;;1,74;07/2026;Serviços ;Exame;40301150;Acido urico - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765930;LAERCIO WELTER MACHADO - 838207;765930;'9750167003795586;LAERCIO WELTER MACHADO;1;0 - TITULAR;10080;;1,86;07/2026;Serviços ;Exame;40301397;Bilirrubinas direta, indireta e total - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765930;LAERCIO WELTER MACHADO - 838207;765930;'9750167003795586;LAERCIO WELTER MACHADO;1;0 - TITULAR;10080;;1,74;07/2026;Serviços ;Exame;40301400;Calcio - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765930;LAERCIO WELTER MACHADO - 838207;765930;'9750167003795586;LAERCIO WELTER MACHADO;1;0 - TITULAR;10080;;4,96;07/2026;Serviços ;Exame;40301419;Calcio ionico - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765930;LAERCIO WELTER MACHADO - 838207;765930;'9750167003795586;LAERCIO WELTER MACHADO;1;0 - TITULAR;10080;;1,74;07/2026;Serviços ;Exame;40301427;Capacidade de fixacao de ferro - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765930;LAERCIO WELTER MACHADO - 838207;765930;'9750167003795586;LAERCIO WELTER MACHADO;1;0 - TITULAR;10080;;3,72;07/2026;Serviços ;Exame;40301583;Colesterol HDL - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765930;LAERCIO WELTER MACHADO - 838207;765930;'9750167003795586;LAERCIO WELTER MACHADO;1;0 - TITULAR;10080;;4,96;07/2026;Serviços ;Exame;40301591;Colesterol LDL - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765930;LAERCIO WELTER MACHADO - 838207;765930;'9750167003795586;LAERCIO WELTER MACHADO;1;0 - TITULAR;10080;;1,74;07/2026;Serviços ;Exame;40301605;Colesterol total - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765930;LAERCIO WELTER MACHADO - 838207;765930;'9750167003795586;LAERCIO WELTER MACHADO;1;0 - TITULAR;10080;;1,74;07/2026;Serviços ;Exame;40301630;Creatinina - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765930;LAERCIO WELTER MACHADO - 838207;765930;'9750167003795586;LAERCIO WELTER MACHADO;1;0 - TITULAR;10080;;2,48;07/2026;Serviços ;Exame;40301990;Gama-glutamil transferase - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765930;LAERCIO WELTER MACHADO - 838207;765930;'9750167003795586;LAERCIO WELTER MACHADO;1;0 - TITULAR;10080;;1,74;07/2026;Serviços ;Exame;40302040;Glicose - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765930;LAERCIO WELTER MACHADO - 838207;765930;'9750167003795586;LAERCIO WELTER MACHADO;1;0 - TITULAR;10080;;8,4;07/2026;Serviços ;Exame;40302075;Hemoglobina glicada A1 total - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765930;LAERCIO WELTER MACHADO - 838207;765930;'9750167003795586;LAERCIO WELTER MACHADO;1;0 - TITULAR;10080;;24,77;07/2026;Serviços ;Exame;40302113;Homocisteina - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765930;LAERCIO WELTER MACHADO - 838207;765930;'9750167003795586;LAERCIO WELTER MACHADO;1;0 - TITULAR;10080;;1,98;07/2026;Serviços ;Exame;40302237;Magnesio - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765930;LAERCIO WELTER MACHADO - 838207;765930;'9750167003795586;LAERCIO WELTER MACHADO;1;0 - TITULAR;10080;;1,74;07/2026;Serviços ;Exame;40302504;Transaminase oxalacetica amino transferase aspartato - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765930;LAERCIO WELTER MACHADO - 838207;765930;'9750167003795586;LAERCIO WELTER MACHADO;1;0 - TITULAR;10080;;1,74;07/2026;Serviços ;Exame;40302512;Transaminase piruvica amino transferase de alanina - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765930;LAERCIO WELTER MACHADO - 838207;765930;'9750167003795586;LAERCIO WELTER MACHADO;1;0 - TITULAR;10080;;2,48;07/2026;Serviços ;Exame;40302547;Triglicerideos - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765930;LAERCIO WELTER MACHADO - 838207;765930;'9750167003795586;LAERCIO WELTER MACHADO;1;0 - TITULAR;10080;;1,74;07/2026;Serviços ;Exame;40302580;Ureia - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765930;LAERCIO WELTER MACHADO - 838207;765930;'9750167003795586;LAERCIO WELTER MACHADO;1;0 - TITULAR;10080;;8,28;07/2026;Serviços ;Exame;40302830;Vitamina D 25 HIDROXI, pesquisa e/ou dosagem Vitamina D3
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765930;LAERCIO WELTER MACHADO - 838207;765930;'9750167003795586;LAERCIO WELTER MACHADO;1;0 - TITULAR;10080;;3,72;07/2026;Serviços ;Exame;40304361;Hemograma com contagem de plaquetas ou fracoes eritrograma, leucograma, plaquet
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765930;LAERCIO WELTER MACHADO - 838207;765930;'9750167003795586;LAERCIO WELTER MACHADO;1;0 - TITULAR;10080;;8,3;07/2026;Serviços ;Exame;40308391;Proteina C reativa, quantitativa - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765930;LAERCIO WELTER MACHADO - 838207;765930;'9750167003795586;LAERCIO WELTER MACHADO;1;0 - TITULAR;10080;;2,48;07/2026;Serviços ;Exame;40311210;Rotina de urina caracteres fisicos, elementos anormais e sedimentoscopia
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765930;LAERCIO WELTER MACHADO - 838207;765930;'9750167003795586;LAERCIO WELTER MACHADO;1;0 - TITULAR;10080;;14,86;07/2026;Serviços ;Exame;40313328;Zinco - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765930;LAERCIO WELTER MACHADO - 838207;765930;'9750167003795586;LAERCIO WELTER MACHADO;1;0 - TITULAR;10080;;15,48;07/2026;Serviços ;Exame;40316270;Ferritina - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765930;LAERCIO WELTER MACHADO - 838207;765930;'9750167003795586;LAERCIO WELTER MACHADO;1;0 - TITULAR;10080;;25,76;07/2026;Serviços ;Exame;40316300;Globulina de ligacao de hormonios sexuais SHBG - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765930;LAERCIO WELTER MACHADO - 838207;765930;'9750167003795586;LAERCIO WELTER MACHADO;1;0 - TITULAR;10080;;6,81;07/2026;Serviços ;Exame;40316360;Insulina - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765930;LAERCIO WELTER MACHADO - 838207;765930;'9750167003795586;LAERCIO WELTER MACHADO;1;0 - TITULAR;10080;;10,53;07/2026;Serviços ;Exame;40316467;T3 livre - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765930;LAERCIO WELTER MACHADO - 838207;765930;'9750167003795586;LAERCIO WELTER MACHADO;1;0 - TITULAR;10080;;10,53;07/2026;Serviços ;Exame;40316491;T4 livre - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765930;LAERCIO WELTER MACHADO - 838207;765930;'9750167003795586;LAERCIO WELTER MACHADO;1;0 - TITULAR;10080;;18,58;07/2026;Serviços ;Exame;40316505;Testosterona livre - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765930;LAERCIO WELTER MACHADO - 838207;765930;'9750167003795586;LAERCIO WELTER MACHADO;1;0 - TITULAR;10080;;13,01;07/2026;Serviços ;Exame;40316513;Testosterona total - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765930;LAERCIO WELTER MACHADO - 838207;765930;'9750167003795586;LAERCIO WELTER MACHADO;1;0 - TITULAR;10080;;10,53;07/2026;Serviços ;Exame;40316521;Tireoestimulante, hormonio TSH - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765930;LAERCIO WELTER MACHADO - 838207;765930;'9750167003795586;LAERCIO WELTER MACHADO;1;0 - TITULAR;10080;;10,53;07/2026;Serviços ;Exame;40316572;Vitamina B12 - pesquisa e/ou dosagem
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765930;LAERCIO WELTER MACHADO - 838207;765912;'9750167003795608;ALESSANDRA APARECIDA WESTENBERGER MACHADO;2;1 - CONJUGE;10080;;541,31;07/2026;Mensalidade;;;
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765930;LAERCIO WELTER MACHADO - 838207;765912;'9750167003795608;ALESSANDRA APARECIDA WESTENBERGER MACHADO;2;1 - CONJUGE;10080;;16,68;07/2026;Serviços ;Exame;40805026;RX - Torax - 2 incidencias
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765930;LAERCIO WELTER MACHADO - 838207;765912;'9750167003795608;ALESSANDRA APARECIDA WESTENBERGER MACHADO;2;1 - CONJUGE;10080;;77,72;07/2026;Serviços ;Exame;40808041;Mamografia digital bilateral com diretriz definida pela ANS - n 52
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765930;LAERCIO WELTER MACHADO - 838207;765912;'9750167003795608;ALESSANDRA APARECIDA WESTENBERGER MACHADO;2;1 - CONJUGE;10080;;59,76;07/2026;Serviços ;Exame;40808130;Densitometria ossea - rotina: coluna e femur ou dois segmentos
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765930;LAERCIO WELTER MACHADO - 838207;765912;'9750167003795608;ALESSANDRA APARECIDA WESTENBERGER MACHADO;2;1 - CONJUGE;10080;;38,52;07/2026;Serviços ;Exame;40901114;US - Mamas
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765930;LAERCIO WELTER MACHADO - 838207;765912;'9750167003795608;ALESSANDRA APARECIDA WESTENBERGER MACHADO;2;1 - CONJUGE;10080;;78,49;07/2026;Serviços ;Exame;40901122;US - Abdome total abdome superior, rins, bexiga, aorta, veia cava inferior e ad
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765930;LAERCIO WELTER MACHADO - 838207;765912;'9750167003795608;ALESSANDRA APARECIDA WESTENBERGER MACHADO;2;1 - CONJUGE;10080;;39,43;07/2026;Serviços ;Exame;40901203;US - Orgaos superficiais tireoide ou escroto ou penis ou cranio
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765930;LAERCIO WELTER MACHADO - 838207;765912;'9750167003795608;ALESSANDRA APARECIDA WESTENBERGER MACHADO;2;1 - CONJUGE;10080;;44,36;07/2026;Serviços ;Exame;40901300;US - Transvaginal utero, ovario, anexos e vagina
5855250-0;3151388;201924839;2778,08;2778,08;Aberto;CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113;765930;LAERCIO WELTER MACHADO - 838207;765913;'9750167003795616;MATHEUS WESTENBERGER MACHADO;2;10 - FILHO(A);10080;;129,61;07/2026;Mensalidade;;;
1 Cód. Contr/Contrat. Pagador Id. Título Número Título Valor Bruto Valor Líquido Situação Título Contratante Id. Titular Família Id. Benef. Cód.Beneficiário Nome Beneficiário Tipo Benef. Grau Depend. Código Combinação Reservado Valor Competência Tipo Tipo Ato Médico Cód. Serviço Descrição Serviço
2 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765929 TATIANE WELTER MACHADO DE CARVALHO - 838206 765929 '9750167003795578 TATIANE WELTER MACHADO DE CARVALHO 1 0 - TITULAR 10080 318,03 07/2026 Mensalidade
3 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765929 TATIANE WELTER MACHADO DE CARVALHO - 838206 765929 '9750167003795578 TATIANE WELTER MACHADO DE CARVALHO 1 0 - TITULAR 10080 1,74 07/2026 Serviços Exame 40301150 Acido urico - pesquisa e/ou dosagem
4 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765929 TATIANE WELTER MACHADO DE CARVALHO - 838206 765929 '9750167003795578 TATIANE WELTER MACHADO DE CARVALHO 1 0 - TITULAR 10080 1,74 07/2026 Serviços Exame 40301400 Calcio - pesquisa e/ou dosagem
5 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765929 TATIANE WELTER MACHADO DE CARVALHO - 838206 765929 '9750167003795578 TATIANE WELTER MACHADO DE CARVALHO 1 0 - TITULAR 10080 14,86 07/2026 Serviços Exame 40301567 Cobre - pesquisa e/ou dosagem
6 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765929 TATIANE WELTER MACHADO DE CARVALHO - 838206 765929 '9750167003795578 TATIANE WELTER MACHADO DE CARVALHO 1 0 - TITULAR 10080 3,72 07/2026 Serviços Exame 40301583 Colesterol HDL - pesquisa e/ou dosagem
7 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765929 TATIANE WELTER MACHADO DE CARVALHO - 838206 765929 '9750167003795578 TATIANE WELTER MACHADO DE CARVALHO 1 0 - TITULAR 10080 4,96 07/2026 Serviços Exame 40301591 Colesterol LDL - pesquisa e/ou dosagem
8 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765929 TATIANE WELTER MACHADO DE CARVALHO - 838206 765929 '9750167003795578 TATIANE WELTER MACHADO DE CARVALHO 1 0 - TITULAR 10080 1,74 07/2026 Serviços Exame 40301630 Creatinina - pesquisa e/ou dosagem
9 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765929 TATIANE WELTER MACHADO DE CARVALHO - 838206 765929 '9750167003795578 TATIANE WELTER MACHADO DE CARVALHO 1 0 - TITULAR 10080 2,23 07/2026 Serviços Exame 40301842 Ferro serico - pesquisa e/ou dosagem
10 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765929 TATIANE WELTER MACHADO DE CARVALHO - 838206 765929 '9750167003795578 TATIANE WELTER MACHADO DE CARVALHO 1 0 - TITULAR 10080 1,74 07/2026 Serviços Exame 40302040 Glicose - pesquisa e/ou dosagem
11 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765929 TATIANE WELTER MACHADO DE CARVALHO - 838206 765929 '9750167003795578 TATIANE WELTER MACHADO DE CARVALHO 1 0 - TITULAR 10080 8,4 07/2026 Serviços Exame 40302075 Hemoglobina glicada A1 total - pesquisa e/ou dosagem
12 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765929 TATIANE WELTER MACHADO DE CARVALHO - 838206 765929 '9750167003795578 TATIANE WELTER MACHADO DE CARVALHO 1 0 - TITULAR 10080 1,98 07/2026 Serviços Exame 40302237 Magnesio - pesquisa e/ou dosagem
13 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765929 TATIANE WELTER MACHADO DE CARVALHO - 838206 765929 '9750167003795578 TATIANE WELTER MACHADO DE CARVALHO 1 0 - TITULAR 10080 1,74 07/2026 Serviços Exame 40302504 Transaminase oxalacetica amino transferase aspartato - pesquisa e/ou dosagem
14 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765929 TATIANE WELTER MACHADO DE CARVALHO - 838206 765929 '9750167003795578 TATIANE WELTER MACHADO DE CARVALHO 1 0 - TITULAR 10080 1,74 07/2026 Serviços Exame 40302512 Transaminase piruvica amino transferase de alanina - pesquisa e/ou dosagem
15 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765929 TATIANE WELTER MACHADO DE CARVALHO - 838206 765929 '9750167003795578 TATIANE WELTER MACHADO DE CARVALHO 1 0 - TITULAR 10080 2,48 07/2026 Serviços Exame 40302547 Triglicerideos - pesquisa e/ou dosagem
16 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765929 TATIANE WELTER MACHADO DE CARVALHO - 838206 765929 '9750167003795578 TATIANE WELTER MACHADO DE CARVALHO 1 0 - TITULAR 10080 1,74 07/2026 Serviços Exame 40302580 Ureia - pesquisa e/ou dosagem
17 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765929 TATIANE WELTER MACHADO DE CARVALHO - 838206 765929 '9750167003795578 TATIANE WELTER MACHADO DE CARVALHO 1 0 - TITULAR 10080 8,8 07/2026 Serviços Exame 40302601 Vitamina A, pesquisa e/ou dosagem
18 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765929 TATIANE WELTER MACHADO DE CARVALHO - 838206 765929 '9750167003795578 TATIANE WELTER MACHADO DE CARVALHO 1 0 - TITULAR 10080 8,28 07/2026 Serviços Exame 40302830 Vitamina D 25 HIDROXI, pesquisa e/ou dosagem Vitamina D3
19 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765929 TATIANE WELTER MACHADO DE CARVALHO - 838206 765929 '9750167003795578 TATIANE WELTER MACHADO DE CARVALHO 1 0 - TITULAR 10080 3,72 07/2026 Serviços Exame 40304361 Hemograma com contagem de plaquetas ou fracoes eritrograma, leucograma, plaquet
20 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765929 TATIANE WELTER MACHADO DE CARVALHO - 838206 765929 '9750167003795578 TATIANE WELTER MACHADO DE CARVALHO 1 0 - TITULAR 10080 24,77 07/2026 Serviços Exame 40305465 Paratormonio - PTH ou fracao cada - pesquisa e/ou dosagem
21 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765929 TATIANE WELTER MACHADO DE CARVALHO - 838206 765929 '9750167003795578 TATIANE WELTER MACHADO DE CARVALHO 1 0 - TITULAR 10080 14,86 07/2026 Serviços Exame 40313328 Zinco - pesquisa e/ou dosagem
22 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765929 TATIANE WELTER MACHADO DE CARVALHO - 838206 765929 '9750167003795578 TATIANE WELTER MACHADO DE CARVALHO 1 0 - TITULAR 10080 9,29 07/2026 Serviços Exame 40316190 Cortisol - pesquisa e/ou dosagem
23 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765929 TATIANE WELTER MACHADO DE CARVALHO - 838206 765929 '9750167003795578 TATIANE WELTER MACHADO DE CARVALHO 1 0 - TITULAR 10080 12,39 07/2026 Serviços Exame 40316246 Estradiol - pesquisa e/ou dosagem
24 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765929 TATIANE WELTER MACHADO DE CARVALHO - 838206 765929 '9750167003795578 TATIANE WELTER MACHADO DE CARVALHO 1 0 - TITULAR 10080 15,48 07/2026 Serviços Exame 40316270 Ferritina - pesquisa e/ou dosagem
25 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765929 TATIANE WELTER MACHADO DE CARVALHO - 838206 765929 '9750167003795578 TATIANE WELTER MACHADO DE CARVALHO 1 0 - TITULAR 10080 8,05 07/2026 Serviços Exame 40316289 Foliculo estimulante, hormonio FSH - pesquisa e/ou dosagem
26 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765929 TATIANE WELTER MACHADO DE CARVALHO - 838206 765929 '9750167003795578 TATIANE WELTER MACHADO DE CARVALHO 1 0 - TITULAR 10080 25,76 07/2026 Serviços Exame 40316300 Globulina de ligacao de hormonios sexuais SHBG - pesquisa e/ou dosagem
27 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765929 TATIANE WELTER MACHADO DE CARVALHO - 838206 765929 '9750167003795578 TATIANE WELTER MACHADO DE CARVALHO 1 0 - TITULAR 10080 7,8 07/2026 Serviços Exame 40316335 Hormonio luteinizante LH - pesquisa e/ou dosagem
28 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765929 TATIANE WELTER MACHADO DE CARVALHO - 838206 765929 '9750167003795578 TATIANE WELTER MACHADO DE CARVALHO 1 0 - TITULAR 10080 6,81 07/2026 Serviços Exame 40316360 Insulina - pesquisa e/ou dosagem
29 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765929 TATIANE WELTER MACHADO DE CARVALHO - 838206 765929 '9750167003795578 TATIANE WELTER MACHADO DE CARVALHO 1 0 - TITULAR 10080 13,01 07/2026 Serviços Exame 40316408 Progesterona - pesquisa e/ou dosagem
30 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765929 TATIANE WELTER MACHADO DE CARVALHO - 838206 765929 '9750167003795578 TATIANE WELTER MACHADO DE CARVALHO 1 0 - TITULAR 10080 10,53 07/2026 Serviços Exame 40316416 Prolactina - pesquisa e/ou dosagem
31 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765929 TATIANE WELTER MACHADO DE CARVALHO - 838206 765929 '9750167003795578 TATIANE WELTER MACHADO DE CARVALHO 1 0 - TITULAR 10080 10,53 07/2026 Serviços Exame 40316491 T4 livre - pesquisa e/ou dosagem
32 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765929 TATIANE WELTER MACHADO DE CARVALHO - 838206 765929 '9750167003795578 TATIANE WELTER MACHADO DE CARVALHO 1 0 - TITULAR 10080 18,58 07/2026 Serviços Exame 40316505 Testosterona livre - pesquisa e/ou dosagem
33 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765929 TATIANE WELTER MACHADO DE CARVALHO - 838206 765929 '9750167003795578 TATIANE WELTER MACHADO DE CARVALHO 1 0 - TITULAR 10080 13,01 07/2026 Serviços Exame 40316513 Testosterona total - pesquisa e/ou dosagem
34 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765929 TATIANE WELTER MACHADO DE CARVALHO - 838206 765929 '9750167003795578 TATIANE WELTER MACHADO DE CARVALHO 1 0 - TITULAR 10080 10,53 07/2026 Serviços Exame 40316521 Tireoestimulante, hormonio TSH - pesquisa e/ou dosagem
35 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765929 TATIANE WELTER MACHADO DE CARVALHO - 838206 765929 '9750167003795578 TATIANE WELTER MACHADO DE CARVALHO 1 0 - TITULAR 10080 8,05 07/2026 Serviços Exame 40316556 Triiodotironina T3 - pesquisa e/ou dosagem
36 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765929 TATIANE WELTER MACHADO DE CARVALHO - 838206 765929 '9750167003795578 TATIANE WELTER MACHADO DE CARVALHO 1 0 - TITULAR 10080 10,53 07/2026 Serviços Exame 40316572 Vitamina B12 - pesquisa e/ou dosagem
37 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765929 TATIANE WELTER MACHADO DE CARVALHO - 838206 765911 '9750167003795594 ALEXANDRE DE CARVALHO 2 1 - CONJUGE 10080 228,72 07/2026 Mensalidade
38 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765929 TATIANE WELTER MACHADO DE CARVALHO - 838206 765914 '9750167003795624 HELOISA MACHADO DE CARVALHO 2 10 - FILHO(A) 10080 129,61 07/2026 Mensalidade
39 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765930 LAERCIO WELTER MACHADO - 838207 765930 '9750167003795586 LAERCIO WELTER MACHADO 1 0 - TITULAR 10080 541,31 07/2026 Mensalidade
40 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765930 LAERCIO WELTER MACHADO - 838207 765930 '9750167003795586 LAERCIO WELTER MACHADO 1 0 - TITULAR 10080 1,74 07/2026 Serviços Exame 40301060 Acido ascorbico vitamina C - pesquisa e/ou dosagem
41 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765930 LAERCIO WELTER MACHADO - 838207 765930 '9750167003795586 LAERCIO WELTER MACHADO 1 0 - TITULAR 10080 10,53 07/2026 Serviços Exame 40301087 Acido folico, pesquisa e/ou dosagem nos eritrocitos
42 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765930 LAERCIO WELTER MACHADO - 838207 765930 '9750167003795586 LAERCIO WELTER MACHADO 1 0 - TITULAR 10080 1,74 07/2026 Serviços Exame 40301150 Acido urico - pesquisa e/ou dosagem
43 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765930 LAERCIO WELTER MACHADO - 838207 765930 '9750167003795586 LAERCIO WELTER MACHADO 1 0 - TITULAR 10080 1,86 07/2026 Serviços Exame 40301397 Bilirrubinas direta, indireta e total - pesquisa e/ou dosagem
44 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765930 LAERCIO WELTER MACHADO - 838207 765930 '9750167003795586 LAERCIO WELTER MACHADO 1 0 - TITULAR 10080 1,74 07/2026 Serviços Exame 40301400 Calcio - pesquisa e/ou dosagem
45 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765930 LAERCIO WELTER MACHADO - 838207 765930 '9750167003795586 LAERCIO WELTER MACHADO 1 0 - TITULAR 10080 4,96 07/2026 Serviços Exame 40301419 Calcio ionico - pesquisa e/ou dosagem
46 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765930 LAERCIO WELTER MACHADO - 838207 765930 '9750167003795586 LAERCIO WELTER MACHADO 1 0 - TITULAR 10080 1,74 07/2026 Serviços Exame 40301427 Capacidade de fixacao de ferro - pesquisa e/ou dosagem
47 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765930 LAERCIO WELTER MACHADO - 838207 765930 '9750167003795586 LAERCIO WELTER MACHADO 1 0 - TITULAR 10080 3,72 07/2026 Serviços Exame 40301583 Colesterol HDL - pesquisa e/ou dosagem
48 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765930 LAERCIO WELTER MACHADO - 838207 765930 '9750167003795586 LAERCIO WELTER MACHADO 1 0 - TITULAR 10080 4,96 07/2026 Serviços Exame 40301591 Colesterol LDL - pesquisa e/ou dosagem
49 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765930 LAERCIO WELTER MACHADO - 838207 765930 '9750167003795586 LAERCIO WELTER MACHADO 1 0 - TITULAR 10080 1,74 07/2026 Serviços Exame 40301605 Colesterol total - pesquisa e/ou dosagem
50 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765930 LAERCIO WELTER MACHADO - 838207 765930 '9750167003795586 LAERCIO WELTER MACHADO 1 0 - TITULAR 10080 1,74 07/2026 Serviços Exame 40301630 Creatinina - pesquisa e/ou dosagem
51 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765930 LAERCIO WELTER MACHADO - 838207 765930 '9750167003795586 LAERCIO WELTER MACHADO 1 0 - TITULAR 10080 2,48 07/2026 Serviços Exame 40301990 Gama-glutamil transferase - pesquisa e/ou dosagem
52 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765930 LAERCIO WELTER MACHADO - 838207 765930 '9750167003795586 LAERCIO WELTER MACHADO 1 0 - TITULAR 10080 1,74 07/2026 Serviços Exame 40302040 Glicose - pesquisa e/ou dosagem
53 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765930 LAERCIO WELTER MACHADO - 838207 765930 '9750167003795586 LAERCIO WELTER MACHADO 1 0 - TITULAR 10080 8,4 07/2026 Serviços Exame 40302075 Hemoglobina glicada A1 total - pesquisa e/ou dosagem
54 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765930 LAERCIO WELTER MACHADO - 838207 765930 '9750167003795586 LAERCIO WELTER MACHADO 1 0 - TITULAR 10080 24,77 07/2026 Serviços Exame 40302113 Homocisteina - pesquisa e/ou dosagem
55 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765930 LAERCIO WELTER MACHADO - 838207 765930 '9750167003795586 LAERCIO WELTER MACHADO 1 0 - TITULAR 10080 1,98 07/2026 Serviços Exame 40302237 Magnesio - pesquisa e/ou dosagem
56 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765930 LAERCIO WELTER MACHADO - 838207 765930 '9750167003795586 LAERCIO WELTER MACHADO 1 0 - TITULAR 10080 1,74 07/2026 Serviços Exame 40302504 Transaminase oxalacetica amino transferase aspartato - pesquisa e/ou dosagem
57 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765930 LAERCIO WELTER MACHADO - 838207 765930 '9750167003795586 LAERCIO WELTER MACHADO 1 0 - TITULAR 10080 1,74 07/2026 Serviços Exame 40302512 Transaminase piruvica amino transferase de alanina - pesquisa e/ou dosagem
58 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765930 LAERCIO WELTER MACHADO - 838207 765930 '9750167003795586 LAERCIO WELTER MACHADO 1 0 - TITULAR 10080 2,48 07/2026 Serviços Exame 40302547 Triglicerideos - pesquisa e/ou dosagem
59 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765930 LAERCIO WELTER MACHADO - 838207 765930 '9750167003795586 LAERCIO WELTER MACHADO 1 0 - TITULAR 10080 1,74 07/2026 Serviços Exame 40302580 Ureia - pesquisa e/ou dosagem
60 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765930 LAERCIO WELTER MACHADO - 838207 765930 '9750167003795586 LAERCIO WELTER MACHADO 1 0 - TITULAR 10080 8,28 07/2026 Serviços Exame 40302830 Vitamina D 25 HIDROXI, pesquisa e/ou dosagem Vitamina D3
61 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765930 LAERCIO WELTER MACHADO - 838207 765930 '9750167003795586 LAERCIO WELTER MACHADO 1 0 - TITULAR 10080 3,72 07/2026 Serviços Exame 40304361 Hemograma com contagem de plaquetas ou fracoes eritrograma, leucograma, plaquet
62 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765930 LAERCIO WELTER MACHADO - 838207 765930 '9750167003795586 LAERCIO WELTER MACHADO 1 0 - TITULAR 10080 8,3 07/2026 Serviços Exame 40308391 Proteina C reativa, quantitativa - pesquisa e/ou dosagem
63 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765930 LAERCIO WELTER MACHADO - 838207 765930 '9750167003795586 LAERCIO WELTER MACHADO 1 0 - TITULAR 10080 2,48 07/2026 Serviços Exame 40311210 Rotina de urina caracteres fisicos, elementos anormais e sedimentoscopia
64 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765930 LAERCIO WELTER MACHADO - 838207 765930 '9750167003795586 LAERCIO WELTER MACHADO 1 0 - TITULAR 10080 14,86 07/2026 Serviços Exame 40313328 Zinco - pesquisa e/ou dosagem
65 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765930 LAERCIO WELTER MACHADO - 838207 765930 '9750167003795586 LAERCIO WELTER MACHADO 1 0 - TITULAR 10080 15,48 07/2026 Serviços Exame 40316270 Ferritina - pesquisa e/ou dosagem
66 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765930 LAERCIO WELTER MACHADO - 838207 765930 '9750167003795586 LAERCIO WELTER MACHADO 1 0 - TITULAR 10080 25,76 07/2026 Serviços Exame 40316300 Globulina de ligacao de hormonios sexuais SHBG - pesquisa e/ou dosagem
67 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765930 LAERCIO WELTER MACHADO - 838207 765930 '9750167003795586 LAERCIO WELTER MACHADO 1 0 - TITULAR 10080 6,81 07/2026 Serviços Exame 40316360 Insulina - pesquisa e/ou dosagem
68 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765930 LAERCIO WELTER MACHADO - 838207 765930 '9750167003795586 LAERCIO WELTER MACHADO 1 0 - TITULAR 10080 10,53 07/2026 Serviços Exame 40316467 T3 livre - pesquisa e/ou dosagem
69 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765930 LAERCIO WELTER MACHADO - 838207 765930 '9750167003795586 LAERCIO WELTER MACHADO 1 0 - TITULAR 10080 10,53 07/2026 Serviços Exame 40316491 T4 livre - pesquisa e/ou dosagem
70 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765930 LAERCIO WELTER MACHADO - 838207 765930 '9750167003795586 LAERCIO WELTER MACHADO 1 0 - TITULAR 10080 18,58 07/2026 Serviços Exame 40316505 Testosterona livre - pesquisa e/ou dosagem
71 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765930 LAERCIO WELTER MACHADO - 838207 765930 '9750167003795586 LAERCIO WELTER MACHADO 1 0 - TITULAR 10080 13,01 07/2026 Serviços Exame 40316513 Testosterona total - pesquisa e/ou dosagem
72 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765930 LAERCIO WELTER MACHADO - 838207 765930 '9750167003795586 LAERCIO WELTER MACHADO 1 0 - TITULAR 10080 10,53 07/2026 Serviços Exame 40316521 Tireoestimulante, hormonio TSH - pesquisa e/ou dosagem
73 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765930 LAERCIO WELTER MACHADO - 838207 765930 '9750167003795586 LAERCIO WELTER MACHADO 1 0 - TITULAR 10080 10,53 07/2026 Serviços Exame 40316572 Vitamina B12 - pesquisa e/ou dosagem
74 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765930 LAERCIO WELTER MACHADO - 838207 765912 '9750167003795608 ALESSANDRA APARECIDA WESTENBERGER MACHADO 2 1 - CONJUGE 10080 541,31 07/2026 Mensalidade
75 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765930 LAERCIO WELTER MACHADO - 838207 765912 '9750167003795608 ALESSANDRA APARECIDA WESTENBERGER MACHADO 2 1 - CONJUGE 10080 16,68 07/2026 Serviços Exame 40805026 RX - Torax - 2 incidencias
76 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765930 LAERCIO WELTER MACHADO - 838207 765912 '9750167003795608 ALESSANDRA APARECIDA WESTENBERGER MACHADO 2 1 - CONJUGE 10080 77,72 07/2026 Serviços Exame 40808041 Mamografia digital bilateral com diretriz definida pela ANS - n 52
77 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765930 LAERCIO WELTER MACHADO - 838207 765912 '9750167003795608 ALESSANDRA APARECIDA WESTENBERGER MACHADO 2 1 - CONJUGE 10080 59,76 07/2026 Serviços Exame 40808130 Densitometria ossea - rotina: coluna e femur ou dois segmentos
78 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765930 LAERCIO WELTER MACHADO - 838207 765912 '9750167003795608 ALESSANDRA APARECIDA WESTENBERGER MACHADO 2 1 - CONJUGE 10080 38,52 07/2026 Serviços Exame 40901114 US - Mamas
79 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765930 LAERCIO WELTER MACHADO - 838207 765912 '9750167003795608 ALESSANDRA APARECIDA WESTENBERGER MACHADO 2 1 - CONJUGE 10080 78,49 07/2026 Serviços Exame 40901122 US - Abdome total abdome superior, rins, bexiga, aorta, veia cava inferior e ad
80 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765930 LAERCIO WELTER MACHADO - 838207 765912 '9750167003795608 ALESSANDRA APARECIDA WESTENBERGER MACHADO 2 1 - CONJUGE 10080 39,43 07/2026 Serviços Exame 40901203 US - Orgaos superficiais tireoide ou escroto ou penis ou cranio
81 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765930 LAERCIO WELTER MACHADO - 838207 765912 '9750167003795608 ALESSANDRA APARECIDA WESTENBERGER MACHADO 2 1 - CONJUGE 10080 44,36 07/2026 Serviços Exame 40901300 US - Transvaginal utero, ovario, anexos e vagina
82 5855250-0 3151388 201924839 2778,08 2778,08 Aberto CHURRASCARIA BUFALO BRANCO LTDA - 81173460000113 765930 LAERCIO WELTER MACHADO - 838207 765913 '9750167003795616 MATHEUS WESTENBERGER MACHADO 2 10 - FILHO(A) 10080 129,61 07/2026 Mensalidade

View File

@ -0,0 +1,17 @@
CODIGOEMPRESA;NOMEFUNC;CPFFUNC;CODIGOOUTEMP;DATAINICIAL;NOMEDEPENDENTE;CPFDEPENDENTE;VALOREMPRESA;VALOR;DESCRICAO
1006;JULIANA APARECIDA DA SILVA FOGACA;006.303.039-08;3755;01/04/2026;;;0;0;
1006;JULIANA APARECIDA DA SILVA FOGACA;006.303.039-08;3755;01/04/2026;BEATRIZ FOGACA HARTHMAN;096.075.699-03;0;0;
1006;VILSO RICARDO DE MELO;026.745.449-02;3755;01/04/2026;BENICIO HUBNER DE MELO;132.814.259-09;0;0;
1006;VILSO RICARDO DE MELO;026.745.449-02;3755;01/04/2026;;;0;0;
1006;VILSO RICARDO DE MELO;026.745.449-02;3755;01/04/2026;CAIRO HUBNER DE MELO;132.814.309-02;0;0;
1006;FELIPE ANDERSON RITTER;041.486.669-00;3755;01/04/2026;;;0;0;
1006;FELIPE ANDERSON RITTER;041.486.669-00;3755;01/04/2026;LETICIA GABRIELLY BIRKHEUER RITTER;126.888.299-20;0;0;
1006;FELIPE ANDERSON RITTER;041.486.669-00;3755;01/04/2026;LIGIA DOMINGUES VIEIRA BIRKHEUER;040.549.419-07;0;0;
1006;FELIPE ANDERSON RITTER;041.486.669-00;3755;01/04/2026;RIAN AUGUSTO BIRKHEUER RITTER;113.325.969-32;0;0;
1006;RODRIGO CORREA MONMA;048.592.079-47;3755;01/07/2026;HELOISA FERNANDES MOHMA;159.800.389-57;0;0;
1006;RODRIGO CORREA MONMA;048.592.079-47;3755;01/07/2026;MURILO FERNANDES MONMA;127.602.329-48;0;0;
1006;RODRIGO CORREA MONMA;048.592.079-47;3755;01/07/2026;TATIANE FERNANDES DA SILVA MONMA;054.124.379-90;0;0;
1006;RODRIGO CORREA MONMA;048.592.079-47;3755;01/07/2026;YURI FERNANDES MONMA;102.708.349-82;0;0;
1006;RODRIGO CORREA MONMA;048.592.079-47;3755;01/07/2026;;;0;0;
1006;GILMAR GILSON GARZELLA;474.476.560-20;3755;01/04/2026;INES TERESINHA MEGIER GARZELLA;478.014.500-78;0;0;
1006;GILMAR GILSON GARZELLA;474.476.560-20;3755;01/04/2026;;;0;0;
1 CODIGOEMPRESA NOMEFUNC CPFFUNC CODIGOOUTEMP DATAINICIAL NOMEDEPENDENTE CPFDEPENDENTE VALOREMPRESA VALOR DESCRICAO
2 1006 JULIANA APARECIDA DA SILVA FOGACA 006.303.039-08 3755 01/04/2026 0 0
3 1006 JULIANA APARECIDA DA SILVA FOGACA 006.303.039-08 3755 01/04/2026 BEATRIZ FOGACA HARTHMAN 096.075.699-03 0 0
4 1006 VILSO RICARDO DE MELO 026.745.449-02 3755 01/04/2026 BENICIO HUBNER DE MELO 132.814.259-09 0 0
5 1006 VILSO RICARDO DE MELO 026.745.449-02 3755 01/04/2026 0 0
6 1006 VILSO RICARDO DE MELO 026.745.449-02 3755 01/04/2026 CAIRO HUBNER DE MELO 132.814.309-02 0 0
7 1006 FELIPE ANDERSON RITTER 041.486.669-00 3755 01/04/2026 0 0
8 1006 FELIPE ANDERSON RITTER 041.486.669-00 3755 01/04/2026 LETICIA GABRIELLY BIRKHEUER RITTER 126.888.299-20 0 0
9 1006 FELIPE ANDERSON RITTER 041.486.669-00 3755 01/04/2026 LIGIA DOMINGUES VIEIRA BIRKHEUER 040.549.419-07 0 0
10 1006 FELIPE ANDERSON RITTER 041.486.669-00 3755 01/04/2026 RIAN AUGUSTO BIRKHEUER RITTER 113.325.969-32 0 0
11 1006 RODRIGO CORREA MONMA 048.592.079-47 3755 01/07/2026 HELOISA FERNANDES MOHMA 159.800.389-57 0 0
12 1006 RODRIGO CORREA MONMA 048.592.079-47 3755 01/07/2026 MURILO FERNANDES MONMA 127.602.329-48 0 0
13 1006 RODRIGO CORREA MONMA 048.592.079-47 3755 01/07/2026 TATIANE FERNANDES DA SILVA MONMA 054.124.379-90 0 0
14 1006 RODRIGO CORREA MONMA 048.592.079-47 3755 01/07/2026 YURI FERNANDES MONMA 102.708.349-82 0 0
15 1006 RODRIGO CORREA MONMA 048.592.079-47 3755 01/07/2026 0 0
16 1006 GILMAR GILSON GARZELLA 474.476.560-20 3755 01/04/2026 INES TERESINHA MEGIER GARZELLA 478.014.500-78 0 0
17 1006 GILMAR GILSON GARZELLA 474.476.560-20 3755 01/04/2026 0 0

View File

@ -0,0 +1,12 @@
CODIGOEMPRESA;NOMEFUNC;CPFFUNC;CODIGOOUTEMP;DATAINICIAL;NOMEDEPENDENTE;CPFDEPENDENTE;VALOREMPRESA;VALOR;DESCRICAO
1084;CRISTIANA BARRETO LOPES;014.928.469-10;4723;01/08/2025;;;0;0;
1084;CRISTIANA BARRETO LOPES;014.928.469-10;4723;01/08/2025;ANNA CAROLINA LOPES WICHOSKI;092.056.569-76;0;0;
1084;CRISTIANA BARRETO LOPES;014.928.469-10;4723;01/08/2025;ANNA VICTORIA LOPES WICHOSKI;092.056.629-41;0;0;
1084;ISAN GUERRERO MORALES;025.746.839-00;4723;01/08/2025;;;0;0;
1084;SUZILAINE ZENATTI MEYER BEZERRA;032.900.149-32;4723;01/04/2026;JOELSON DA SILVA BEZERRA;023.089.899-80;0;0;
1084;SUZILAINE ZENATTI MEYER BEZERRA;032.900.149-32;4723;01/04/2026;;;0;0;
1084;SUZILAINE ZENATTI MEYER BEZERRA;032.900.149-32;4723;01/04/2026;JOAO LUCAS MEYER BEZERRA;118.021.609-14;0;0;
1084;LILIAN DE ARAUJO NUNEZ;073.890.749-90;4723;01/02/2026;HELOISA NUNES RAMBO;147.388.109-95;0;0;
1084;LILIAN DE ARAUJO NUNEZ;073.890.749-90;4723;01/02/2026;;;0;0;
1084;LILIAN DE ARAUJO NUNEZ;073.890.749-90;4723;01/02/2026;ALEX FERNANDO RAMBO;070.352.959-50;0;0;
1084;LILIAN DE ARAUJO NUNEZ;073.890.749-90;4723;01/02/2026;EUNICE FATIMA PADILHA RAMBO;524.044.399-87;0;0;
1 CODIGOEMPRESA NOMEFUNC CPFFUNC CODIGOOUTEMP DATAINICIAL NOMEDEPENDENTE CPFDEPENDENTE VALOREMPRESA VALOR DESCRICAO
2 1084 CRISTIANA BARRETO LOPES 014.928.469-10 4723 01/08/2025 0 0
3 1084 CRISTIANA BARRETO LOPES 014.928.469-10 4723 01/08/2025 ANNA CAROLINA LOPES WICHOSKI 092.056.569-76 0 0
4 1084 CRISTIANA BARRETO LOPES 014.928.469-10 4723 01/08/2025 ANNA VICTORIA LOPES WICHOSKI 092.056.629-41 0 0
5 1084 ISAN GUERRERO MORALES 025.746.839-00 4723 01/08/2025 0 0
6 1084 SUZILAINE ZENATTI MEYER BEZERRA 032.900.149-32 4723 01/04/2026 JOELSON DA SILVA BEZERRA 023.089.899-80 0 0
7 1084 SUZILAINE ZENATTI MEYER BEZERRA 032.900.149-32 4723 01/04/2026 0 0
8 1084 SUZILAINE ZENATTI MEYER BEZERRA 032.900.149-32 4723 01/04/2026 JOAO LUCAS MEYER BEZERRA 118.021.609-14 0 0
9 1084 LILIAN DE ARAUJO NUNEZ 073.890.749-90 4723 01/02/2026 HELOISA NUNES RAMBO 147.388.109-95 0 0
10 1084 LILIAN DE ARAUJO NUNEZ 073.890.749-90 4723 01/02/2026 0 0
11 1084 LILIAN DE ARAUJO NUNEZ 073.890.749-90 4723 01/02/2026 ALEX FERNANDO RAMBO 070.352.959-50 0 0
12 1084 LILIAN DE ARAUJO NUNEZ 073.890.749-90 4723 01/02/2026 EUNICE FATIMA PADILHA RAMBO 524.044.399-87 0 0

View File

@ -0,0 +1,4 @@
CODIGOEMPRESA;NOMEFUNC;CPFFUNC;CODIGOOUTEMP;DATAINICIAL;NOMEDEPENDENTE;CPFDEPENDENTE;VALOREMPRESA;VALOR;DESCRICAO
128;LAERCIO WELTER MACHADO;968.229.109-72;5060;01/03/2026;;;0;0;
128;LAERCIO WELTER MACHADO;968.229.109-72;5060;01/03/2026;ALESSANDRA APARECIDA WESTENBERGER MACHADO;995.819.909-25;0;0;
128;LAERCIO WELTER MACHADO;968.229.109-72;5060;01/03/2026;MATHEUS WESTENBERGER MACHADO;104.587.539-20;0;0;
1 CODIGOEMPRESA NOMEFUNC CPFFUNC CODIGOOUTEMP DATAINICIAL NOMEDEPENDENTE CPFDEPENDENTE VALOREMPRESA VALOR DESCRICAO
2 128 LAERCIO WELTER MACHADO 968.229.109-72 5060 01/03/2026 0 0
3 128 LAERCIO WELTER MACHADO 968.229.109-72 5060 01/03/2026 ALESSANDRA APARECIDA WESTENBERGER MACHADO 995.819.909-25 0 0
4 128 LAERCIO WELTER MACHADO 968.229.109-72 5060 01/03/2026 MATHEUS WESTENBERGER MACHADO 104.587.539-20 0 0

View File

@ -0,0 +1,5 @@
CODIGOEMPRESA;NOMEFUNC;CPFFUNC;CODIGOOUTEMP;DATAINICIAL;NOMEDEPENDENTE;CPFDEPENDENTE;VALOREMPRESA;VALOR;DESCRICAO
1601;ALINE PATRICIA RAMOS;076.350.759-85;4709;01/07/2026;;;0;0;
1601;DANTE HENRIQUE MUELLER;477.250.019-72;4709;01/07/2026;;;0;0;
1601;DANTE HENRIQUE MUELLER;477.250.019-72;4709;01/07/2026;CAIO PEREIRA MUELLER;053.273.449-16;0;0;
1601;DANTE HENRIQUE MUELLER;477.250.019-72;4709;01/07/2026;MARLENE PEREIRA MUELLER;466.162.920-53;0;0;
1 CODIGOEMPRESA NOMEFUNC CPFFUNC CODIGOOUTEMP DATAINICIAL NOMEDEPENDENTE CPFDEPENDENTE VALOREMPRESA VALOR DESCRICAO
2 1601 ALINE PATRICIA RAMOS 076.350.759-85 4709 01/07/2026 0 0
3 1601 DANTE HENRIQUE MUELLER 477.250.019-72 4709 01/07/2026 0 0
4 1601 DANTE HENRIQUE MUELLER 477.250.019-72 4709 01/07/2026 CAIO PEREIRA MUELLER 053.273.449-16 0 0
5 1601 DANTE HENRIQUE MUELLER 477.250.019-72 4709 01/07/2026 MARLENE PEREIRA MUELLER 466.162.920-53 0 0

View File

@ -0,0 +1,3 @@
CODIGOEMPRESA;NOMEFUNC;CPFFUNC;CODIGOOUTEMP;DATAINICIAL;NOMEDEPENDENTE;CPFDEPENDENTE;VALOREMPRESA;VALOR;DESCRICAO
1604;BRUNO HENRIQUE BRAGA TONELLO;006.296.199-30;3755;01/01/2024;;;0;0;
1604;AMANDA GRACIELE CARVALHO DA SILVA;108.755.529-94;3755;01/04/2026;;;0;0;
1 CODIGOEMPRESA NOMEFUNC CPFFUNC CODIGOOUTEMP DATAINICIAL NOMEDEPENDENTE CPFDEPENDENTE VALOREMPRESA VALOR DESCRICAO
2 1604 BRUNO HENRIQUE BRAGA TONELLO 006.296.199-30 3755 01/01/2024 0 0
3 1604 AMANDA GRACIELE CARVALHO DA SILVA 108.755.529-94 3755 01/04/2026 0 0

View File

@ -0,0 +1,18 @@
CODIGOEMPRESA;NOMEFUNC;CPFFUNC;CODIGOOUTEMP;DATAINICIAL;NOMEDEPENDENTE;CPFDEPENDENTE;VALOREMPRESA;VALOR;DESCRICAO
2028;YUNIOR CABRERA PEREZ;026.272.649-15;4723;01/08/2025;;;0;0;
2028;ERICKE PECORARI HOSSAIN;036.700.922-69;4723;01/08/2025;FLAVIA DOS REIS AHRENFELD HOSSAIN;101.759.839-84;0;0;
2028;ERICKE PECORARI HOSSAIN;036.700.922-69;4723;01/08/2025;;;0;0;
2028;RAFAEL TABORDA;082.004.769-44;4723;01/08/2025;;;0;0;
2028;LUIZ FERNANDO GONCALVES DA SILVA;082.193.739-11;4723;01/08/2025;TALITA ELISSAMA PIRES DA SILVA;092.513.939-40;0;0;
2028;LUIZ FERNANDO GONCALVES DA SILVA;082.193.739-11;4723;01/08/2025;;;0;0;
2028;LUIZ FERNANDO GONCALVES DA SILVA;082.193.739-11;4723;01/08/2025;CHRISTOPHER ALEXANDRE PIRES DA SILVA;140.577.939-09;0;0;
2028;LUIZ FERNANDO GONCALVES DA SILVA;082.193.739-11;4723;01/08/2025;MIGUEL FERNANDO PIRES DA SILVA;165.271.179-14;0;0;
2028;JANDIR MACHADO DA SILVA NETO;114.299.909-29;4723;01/08/2025;ANDRESSA DA SILVA PINHEIRO;106.505.229-40;0;0;
2028;JANDIR MACHADO DA SILVA NETO;114.299.909-29;4723;01/08/2025;;;0;0;
2028;HUMBERTO JOSE RODRIGUEZ SALAZAR;706.573.892-26;4723;01/07/2026;;;0;0;
2028;HUMBERTO JOSE RODRIGUEZ SALAZAR;706.573.892-26;4723;01/07/2026;GABRIELA ANDREINA MENDEZ GUERRA;802.379.739-52;0;0;
2028;ESTEBAN GABRIEL ESPINOLA;802.815.069-14;4723;01/01/2026;;;0;0;
2028;ESTEBAN GABRIEL ESPINOLA;802.815.069-14;4723;01/01/2026;MONICA RAFAELA SARASUA;122.834.911-85;0;0;
2028;ESTEBAN GABRIEL ESPINOLA;802.815.069-14;4723;01/01/2026;IAN GABRIEL ESPINOLA;125.171.311-47;0;0;
2028;REGINA FERNANDES;886.198.779-68;4723;01/08/2025;SOPHIA FERNANDES GONÇALVES;097.256.679-13;0;0;
2028;REGINA FERNANDES;886.198.779-68;4723;01/08/2025;;;0;0;
1 CODIGOEMPRESA NOMEFUNC CPFFUNC CODIGOOUTEMP DATAINICIAL NOMEDEPENDENTE CPFDEPENDENTE VALOREMPRESA VALOR DESCRICAO
2 2028 YUNIOR CABRERA PEREZ 026.272.649-15 4723 01/08/2025 0 0
3 2028 ERICKE PECORARI HOSSAIN 036.700.922-69 4723 01/08/2025 FLAVIA DOS REIS AHRENFELD HOSSAIN 101.759.839-84 0 0
4 2028 ERICKE PECORARI HOSSAIN 036.700.922-69 4723 01/08/2025 0 0
5 2028 RAFAEL TABORDA 082.004.769-44 4723 01/08/2025 0 0
6 2028 LUIZ FERNANDO GONCALVES DA SILVA 082.193.739-11 4723 01/08/2025 TALITA ELISSAMA PIRES DA SILVA 092.513.939-40 0 0
7 2028 LUIZ FERNANDO GONCALVES DA SILVA 082.193.739-11 4723 01/08/2025 0 0
8 2028 LUIZ FERNANDO GONCALVES DA SILVA 082.193.739-11 4723 01/08/2025 CHRISTOPHER ALEXANDRE PIRES DA SILVA 140.577.939-09 0 0
9 2028 LUIZ FERNANDO GONCALVES DA SILVA 082.193.739-11 4723 01/08/2025 MIGUEL FERNANDO PIRES DA SILVA 165.271.179-14 0 0
10 2028 JANDIR MACHADO DA SILVA NETO 114.299.909-29 4723 01/08/2025 ANDRESSA DA SILVA PINHEIRO 106.505.229-40 0 0
11 2028 JANDIR MACHADO DA SILVA NETO 114.299.909-29 4723 01/08/2025 0 0
12 2028 HUMBERTO JOSE RODRIGUEZ SALAZAR 706.573.892-26 4723 01/07/2026 0 0
13 2028 HUMBERTO JOSE RODRIGUEZ SALAZAR 706.573.892-26 4723 01/07/2026 GABRIELA ANDREINA MENDEZ GUERRA 802.379.739-52 0 0
14 2028 ESTEBAN GABRIEL ESPINOLA 802.815.069-14 4723 01/01/2026 0 0
15 2028 ESTEBAN GABRIEL ESPINOLA 802.815.069-14 4723 01/01/2026 MONICA RAFAELA SARASUA 122.834.911-85 0 0
16 2028 ESTEBAN GABRIEL ESPINOLA 802.815.069-14 4723 01/01/2026 IAN GABRIEL ESPINOLA 125.171.311-47 0 0
17 2028 REGINA FERNANDES 886.198.779-68 4723 01/08/2025 SOPHIA FERNANDES GONÇALVES 097.256.679-13 0 0
18 2028 REGINA FERNANDES 886.198.779-68 4723 01/08/2025 0 0

View File

@ -0,0 +1,8 @@
CODIGOEMPRESA;NOMEFUNC;CPFFUNC;CODIGOOUTEMP;DATAINICIAL;NOMEDEPENDENTE;CPFDEPENDENTE;VALOREMPRESA;VALOR;DESCRICAO
626;VANESSA LOPES GOMES;007.998.369-31;3755;01/05/2026;;;0;0;
626;ZAINAB MARIAM HAGE GONZALEZ;014.261.989-22;3755;01/06/2026;;;0;0;
626;BRUNA ALMEIDA BEVILAQUA;123.061.329-33;3755;01/07/2026;;;0;0;
626;JAMAL ABDEL FAYYAT;136.100.328-63;3755;01/07/2026;TAREK CASTRO FAYYAT;077.252.129-81;0;0;
626;JAMAL ABDEL FAYYAT;136.100.328-63;3755;01/07/2026;;;0;0;
626;JAMAL ABDEL FAYYAT;136.100.328-63;3755;01/07/2026;ELLEN LUCE RESENDE CASTRO FAYYAT;988.874.326-00;0;0;
626;NEURI DUARTE GARCIA;662.104.709-63;3755;01/05/2026;;;0;0;
1 CODIGOEMPRESA NOMEFUNC CPFFUNC CODIGOOUTEMP DATAINICIAL NOMEDEPENDENTE CPFDEPENDENTE VALOREMPRESA VALOR DESCRICAO
2 626 VANESSA LOPES GOMES 007.998.369-31 3755 01/05/2026 0 0
3 626 ZAINAB MARIAM HAGE GONZALEZ 014.261.989-22 3755 01/06/2026 0 0
4 626 BRUNA ALMEIDA BEVILAQUA 123.061.329-33 3755 01/07/2026 0 0
5 626 JAMAL ABDEL FAYYAT 136.100.328-63 3755 01/07/2026 TAREK CASTRO FAYYAT 077.252.129-81 0 0
6 626 JAMAL ABDEL FAYYAT 136.100.328-63 3755 01/07/2026 0 0
7 626 JAMAL ABDEL FAYYAT 136.100.328-63 3755 01/07/2026 ELLEN LUCE RESENDE CASTRO FAYYAT 988.874.326-00 0 0
8 626 NEURI DUARTE GARCIA 662.104.709-63 3755 01/05/2026 0 0

View File

@ -0,0 +1,22 @@
CODIGOEMPRESA;NOMEFUNC;CPFFUNC;CODIGOOUTEMP;DATAINICIAL;NOMEDEPENDENTE;CPFDEPENDENTE;VALOREMPRESA;VALOR;DESCRICAO
92;JEAN RUBEN SOUZA JARA;020.998.489-98;5060;01/06/2026;;;0;0;
92;JEAN RUBEN SOUZA JARA;020.998.489-98;5060;01/06/2026;GIOVANNA FOTIADIS HENRIQUES JARA;119.377.409-86;0;0;
92;JEAN RUBEN SOUZA JARA;020.998.489-98;5060;01/06/2026;PATRICIA VALESKA FOTIADIS HENRIQUES JARA;022.498.579-54;0;0;
92;MARCOS AURELIO FABRIN;072.972.289-90;5060;01/06/2026;;;0;0;
92;MARCOS AURELIO FABRIN;072.972.289-90;5060;01/06/2026;EDUARDA VALENTINA FABRIN;166.831.669-27;0;0;
92;MARCOS AURELIO FABRIN;072.972.289-90;5060;01/06/2026;CRISTINA PEREIRA MORENO FABRIN;075.055.819-96;0;0;
92;LEANDRO MARAFON ALVES;074.466.139-04;5060;01/06/2026;;;0;0;
92;LEANDRO MARAFON ALVES;074.466.139-04;5060;01/06/2026;ALINE RAMOS DO NASCIMENTO MARAFON;070.992.059-86;0;0;
92;LEANDRO MARAFON ALVES;074.466.139-04;5060;01/06/2026;LEONARDO DO NASCIMENTO MARAFON ALVES;161.211.439-32;0;0;
92;LEANDRO MARAFON ALVES;074.466.139-04;5060;01/06/2026;LORENA DO NASCIMENTO MARAFON ALVES;153.856.289-80;0;0;
92;MATHEUS PRESCINOTTI;100.083.979-61;5060;01/06/2026;;;0;0;
92;MAURICIO FABRIN;118.554.739-84;5060;01/06/2026;;;0;0;
92;MAURICIO FABRIN;118.554.739-84;5060;01/06/2026;RUBIA MILENA MIGLIOLI REIS;014.006.829-59;0;0;
92;MAURICIO FABRIN;118.554.739-84;5060;01/06/2026;SAMUEL MIGLIOLI FABRIN;101.679.789-34;0;0;
92;MAURICIO FABRIN;118.554.739-84;5060;01/06/2026;ELOAH VITORIA MIGLIOLI FABRIN;167.433.629-21;0;0;
92;ENALTO FERREIRA MIRANDA;494.583.299-49;5060;01/06/2026;;;0;0;
92;ENALTO FERREIRA MIRANDA;494.583.299-49;5060;01/06/2026;EDERSON LIMA MIRANDA;091.686.449-90;0;0;
92;ENALTO FERREIRA MIRANDA;494.583.299-49;5060;01/06/2026;ELAINE LIMA MIRANDA;079.964.309-26;0;0;
92;VALDECIR PROVIN;717.710.269-87;5060;01/06/2026;;;0;0;
92;VALDECIR PROVIN;717.710.269-87;5060;01/06/2026;MAURICIO PROVIN NETO;089.454.879-48;0;0;
92;VALDECIR PROVIN;717.710.269-87;5060;01/06/2026;SUZANA DE ALMEIDA;008.621.539-64;0;0;
1 CODIGOEMPRESA NOMEFUNC CPFFUNC CODIGOOUTEMP DATAINICIAL NOMEDEPENDENTE CPFDEPENDENTE VALOREMPRESA VALOR DESCRICAO
2 92 JEAN RUBEN SOUZA JARA 020.998.489-98 5060 01/06/2026 0 0
3 92 JEAN RUBEN SOUZA JARA 020.998.489-98 5060 01/06/2026 GIOVANNA FOTIADIS HENRIQUES JARA 119.377.409-86 0 0
4 92 JEAN RUBEN SOUZA JARA 020.998.489-98 5060 01/06/2026 PATRICIA VALESKA FOTIADIS HENRIQUES JARA 022.498.579-54 0 0
5 92 MARCOS AURELIO FABRIN 072.972.289-90 5060 01/06/2026 0 0
6 92 MARCOS AURELIO FABRIN 072.972.289-90 5060 01/06/2026 EDUARDA VALENTINA FABRIN 166.831.669-27 0 0
7 92 MARCOS AURELIO FABRIN 072.972.289-90 5060 01/06/2026 CRISTINA PEREIRA MORENO FABRIN 075.055.819-96 0 0
8 92 LEANDRO MARAFON ALVES 074.466.139-04 5060 01/06/2026 0 0
9 92 LEANDRO MARAFON ALVES 074.466.139-04 5060 01/06/2026 ALINE RAMOS DO NASCIMENTO MARAFON 070.992.059-86 0 0
10 92 LEANDRO MARAFON ALVES 074.466.139-04 5060 01/06/2026 LEONARDO DO NASCIMENTO MARAFON ALVES 161.211.439-32 0 0
11 92 LEANDRO MARAFON ALVES 074.466.139-04 5060 01/06/2026 LORENA DO NASCIMENTO MARAFON ALVES 153.856.289-80 0 0
12 92 MATHEUS PRESCINOTTI 100.083.979-61 5060 01/06/2026 0 0
13 92 MAURICIO FABRIN 118.554.739-84 5060 01/06/2026 0 0
14 92 MAURICIO FABRIN 118.554.739-84 5060 01/06/2026 RUBIA MILENA MIGLIOLI REIS 014.006.829-59 0 0
15 92 MAURICIO FABRIN 118.554.739-84 5060 01/06/2026 SAMUEL MIGLIOLI FABRIN 101.679.789-34 0 0
16 92 MAURICIO FABRIN 118.554.739-84 5060 01/06/2026 ELOAH VITORIA MIGLIOLI FABRIN 167.433.629-21 0 0
17 92 ENALTO FERREIRA MIRANDA 494.583.299-49 5060 01/06/2026 0 0
18 92 ENALTO FERREIRA MIRANDA 494.583.299-49 5060 01/06/2026 EDERSON LIMA MIRANDA 091.686.449-90 0 0
19 92 ENALTO FERREIRA MIRANDA 494.583.299-49 5060 01/06/2026 ELAINE LIMA MIRANDA 079.964.309-26 0 0
20 92 VALDECIR PROVIN 717.710.269-87 5060 01/06/2026 0 0
21 92 VALDECIR PROVIN 717.710.269-87 5060 01/06/2026 MAURICIO PROVIN NETO 089.454.879-48 0 0
22 92 VALDECIR PROVIN 717.710.269-87 5060 01/06/2026 SUZANA DE ALMEIDA 008.621.539-64 0 0

849
plano.md Normal file
View File

@ -0,0 +1,849 @@
# Plano — Portal De Paula (Protótipo Interno)
## Visão geral
Reformulação do portal interno da De Paula Contadores. O pedido original foi
"estruturar um protótipo visual, mas funcional, para avaliarmos e
estruturarmos melhor o frontend do portal" — reaproveitando a essência do
portal atual (referência: capturas de tela do sistema em produção, tema
escuro roxo/dourado, sidebar com dezenas de seções, busca de aplicações,
cards de atalho, login com logo circular), não recriando do zero.
Decisões de arquitetura tomadas no início e válidas até a rodada 13:
- **Stack: HTML + CSS + JS puro**, sem framework, sem build step, sem
dependência externa além das fontes do sistema.
- **Sem backend**: todo o estado (sessão, usuários, perfis de acesso,
favoritos, widgets, compromissos, tema) vivia no `localStorage` do
navegador.
- **Visual**: modernizar a estrutura existente (sidebar + topbar + busca +
cards + login), não clonar pixel a pixel as capturas de referência.
Isso valeu como protótipo de validação de fluxo/desenho. Na rodada 13 o
usuário definiu a stack real de backend e pediu a migração completa — ver
essa seção para a arquitetura atual. Este documento foca no histórico de
decisões, no que foi construído em cada rodada e no que ainda está em
aberto; para arquitetura técnica atual (páginas, API, ordem de scripts,
modelo de permissões, como o CSS está dividido entre arquivos), ver
`CLAUDE.md`.
## Cronologia de construção
### 1. Protótipo inicial — login + shell
Tela de login (logo, usuário/senha, "esqueceu senha" apontando para
Integração e Inovação) e o shell principal: sidebar com as 14 seções do
documento original de arquitetura, topbar com hamburger/"Acessar
Ramais"/notificações, busca de aplicações e uma grade de cards de atalho
fixos (Links, OS, Ramais, Relatórios, Calendário, Solicitações). Tema
claro/escuro com toggle salvo em `localStorage`. Um modal de "simular perfil
de acesso" (persona) deixava escolher entre as 7 personas do documento
original para ver o menu mudar — mecanismo **removido depois** (ver item 4).
### 2. Perfis de Acesso
Tela de administração (`perfis-acesso.html`) com lista + edição de perfis:
Código, Nome, árvore de permissões por módulo, aba "Usuários do Escritório".
Criado um perfil especial **"Integração e Inovação"**, com acesso total e
uma capacidade exclusiva — `gerenciaPermissoes` — que também gate-ia o
acesso à própria tela de Perfis de Acesso.
### 3. Usuários
Tela de cadastro de contas (`usuarios.html`), movida para dentro do grupo
"Administração" no menu (junto de Perfis de Acesso), com a mesma restrição:
só quem tem `gerenciaPermissoes` acessa e pode cadastrar/editar/excluir
usuários.
### 4. Login real (fim do simulador de persona)
O usuário pediu para poder testar de verdade o cadastro de acessos —
"podemos testar na realidade como irá funcionar". Isso trocou o mecanismo
inteiro:
- Login parou de aceitar qualquer senha; passou a validar contra contas
reais em `pid_users_v1`/`v2` (`assets/js/users.js`).
- Duas contas de demonstração criadas: `gabriel`/`gabriel` (perfil
Integração e Inovação) e `bruno`/`bruno` (sem perfil vinculado, para
testar o estado "sem acesso").
- O modal de simulação de persona foi **removido**; a visibilidade do menu
passou a vir do perfil de acesso de verdade vinculado ao usuário logado
(`access.js` → `pidResolveAccess`).
- Cada um dos 7 perfis do documento original ganhou um registro real em
Perfis de Acesso (Diretoria, Departamento Pessoal, Gerencial, Legalização,
Fisco/Contábil, Financeiro, Protocolo), mais Integração e Inovação (8 ao
todo).
### 5. Múltiplos perfis por usuário
Pedido explícito: "algumas pessoas podem ter acesso ao perfil financeiro e
fisco/contábil". `profileCodigo` (número único) virou `profileCodigos`
(array) em `pid_users`; a resolução de acesso em `access.js` passou a fazer
**união** entre todos os perfis vinculados a um usuário — uma seção aparece
se qualquer um dos perfis do usuário conceder acesso a ela.
### 6. Bugs encontrados e corrigidos
Três bugs reais surgiram e foram corrigidos durante o uso:
- **`users-admin.js` ficou preso na assinatura antiga** de
`pidResolveAccess()` (`{ profile }` em vez de `{ profiles }`) depois da
migração do item 5 — a tela de Usuários redirecionava sempre de volta
pro Principal antes de renderizar, dando a impressão de "não abre".
Corrigido ajustando a desestruturação.
- **CSS sobrescrevendo o atributo `hidden`**: componentes que definem seu
próprio `display` (`.no-access`, `.notif-badge`, `.app-card`) ignoravam o
`hidden` do JS, porque uma regra de autor com `display` explícito vence a
regra padrão do navegador `[hidden]{display:none}` mesmo com
especificidade igual. A mensagem de "sem perfil vinculado" continuava
aparecendo por cima do dashboard normal mesmo com o perfil já vinculado.
Corrigido com uma regra global em `base.css`:
`[hidden] { display: none !important; }` — resolveu esse caso e mais dois
latentes (badge de notificação, filtro de busca nos cards).
- **Submenus recolhidos continuavam clicáveis/focáveis**: o accordion do
sidebar só usava `max-height:0` + `overflow:hidden` para esconder, o que
não impede clique nem navegação por Tab. Corrigido adicionando
`visibility:hidden` + `pointer-events:none` ao estado fechado.
### 7. Permissão granular por aplicação
A árvore de permissões de cada módulo mostrava ações genéricas
(Visualizar/Incluir/Editar/Excluir), sem relação com o conteúdo real do
menu. Trocado por uma lista das aplicações reais de cada seção (ex.: em
Portais, os checkboxes viraram "Portal do Cliente" e "Portal Fiscal") —
e isso passou a **funcionar de verdade**: desmarcar uma aplicação esconde
só aquele item do sidebar (`data-app` + `access.js`), não o módulo inteiro.
Nesta mesma rodada, o campo **Ambiente** (Escritório/Empresa) foi removido
do cadastro de perfil — "trabalharemos como portal interno, sem mais de um
ambiente".
### 8. Favoritos
Pedido: "toda aplicação deve poder ser favoritada, para adicionar na tela
inicial". Implementado sem precisar marcar cada item do menu manualmente —
`favorites.js` deriva um ID estável a partir do próprio texto/estrutura do
sidebar (ver `CLAUDE.md` → "Favoritos") e injeta uma estrela ao lado de
cada aplicação. A tela Principal deixou de ter cards fixos (Links, OS,
Ramais...) e passou a ser inteiramente dirigida pelos favoritos de cada
usuário — o antigo `cards.js` foi removido.
### 9. Calendário De Paula → link externo
"O Calendário De Paula deve ser um redirecionador" para
`https://depaula-tvcorporativa.lovable.app/calendario` (abre em nova aba).
O calendário interno mockado (`calendario.html` + `calendar.js`, com
eventos fixos por setor) foi removido a pedido do usuário depois da troca —
não recriar sem confirmar.
### 10. Calendário Individual + Widgets
Pedido novo, em duas partes:
- **Calendário Individual** (`calendario-individual.html`): agenda pessoal
de cada colaborador (seção base, disponível a todo perfil). Cada um cria
compromissos e pode marcá-los como visíveis para todos que compartilham
pelo menos um perfil de acesso em comum (`sharedWithProfile` +
`pidEventsVisibleTo` em `events.js`) — colegas só visualizam, não editam
nem excluem compromisso alheio.
- **Widgets** na tela Principal: área "Widgets" com botão "Adicionar
Widget" (seletor, no estilo Asana) — pensada para crescer além de um
único tipo. Hoje só existe o widget de Calendário Individual (próximos 5
compromissos), mas o registro `PID_WIDGET_TYPES` já está estruturado para
novos tipos sem refazer a área de adicionar/remover.
### 11. Horário e edição no Calendário Individual
Compromissos ganharam campo de horário (ordenados por horário dentro do
dia) e passaram a ser editáveis depois de criados — clicar num compromisso
próprio abre o mesmo modal preenchido, com um botão "Excluir" adicional,
em vez de excluir direto ao clicar.
### 12. Menu de conta
O chip do canto superior direito mostrava o **perfil/departamento**
("Departamento Pessoal" etc.) — trocado para mostrar o **usuário logado**
(nome + ícone de pessoa). Clicar abre um menu com nome + perfis vinculados,
"Alterar senha" (modal com senha atual/nova/confirmação, validada contra
`pid_users`) e, só para quem tem `gerenciaPermissoes`, um atalho para
Perfis de Acesso (antes era o clique direto no chip que levava pra lá).
Nesta rodada os estilos de campo de modal (`.modal-field`, `.modal-actions`
etc.), que só existiam em `calendario.css`, foram generalizados para
`components.css`, já que agora mais de uma tela precisa deles.
### 13. Migração para backend Django + PostgreSQL
O usuário definiu a stack real do backend: **Python 3.13 + Django 6.0 +
PostgreSQL 14**, e pediu três coisas na mesma rodada:
1. Construir esse backend completo (models, API REST, banco Postgres) —
não só adaptar o frontend a um contrato assumido.
2. Migrar para o banco **tudo** que hoje existia no `localStorage`, exceto
a preferência de tema: usuários, perfis de acesso, favoritos, widgets e
compromissos do Calendário Individual.
3. Autenticação via **sessão/cookie do Django** (decisão explícita, não
token/JWT) — o que levou à escolha de servir o frontend estático pelo
próprio Django (mesma origem), evitando complicação de cookie de sessão
cross-origin.
O que mudou estruturalmente:
- Projeto Django novo, inicialmente em `Portal/backend/` (`config/` + app
único `portal_api/`) — reorganizado na rodada 14 para o padrão de pastas
de um projeto Django de verdade. Ver `CLAUDE.md` → "Arquitetura" para o
detalhamento de cada arquivo e a tabela de endpoints.
- Model de usuário customizado (`Usuario`, estendendo `AbstractUser`) desde
o início — trocar depois de criado o projeto é doloroso.
- Senha passou a usar hashing nativo do Django (`set_password`/
`check_password`) — antes era texto puro no protótipo; melhoria de
segurança real, não só troca de armazenamento.
- Catálogo de módulos/aplicações do menu (antes `PID_MODULES`/
`PID_MODULE_APPS` hardcoded em `profiles.js`) virou `portal_api/catalogo.py`,
fonte única da verdade, exposto só leitura via `GET /api/catalogo/`.
- A união de permissões entre múltiplos perfis de um usuário (rodada 5),
antes calculada no cliente (`access.js` → `pidResolveAccess`/
`pidApplyAccessVisibility`), passou a ser calculada uma única vez no
servidor (`permissoes_efetivas()` em `views.py`) e consumida pronta via
`GET /api/me/`.
- A lógica de "quem vê qual compromisso" do Calendário Individual (rodada
10 — `pidEventsVisibleTo`) também migrou pro backend
(`CompromissoAgendaViewSet.get_queryset`).
- Todo o frontend que lia/escrevia `localStorage` foi reescrito para
`async`/`fetch` contra a API (`assets/js/api.js`, hoje `static/js/api.js`
— ver rodada 14 — é o módulo novo), mantendo a mesma estrutura de
arquivos e nomes de função sempre que possível para minimizar o tamanho
do diff.
- `assets/js/users.js` (armazenamento local de usuários) foi **removido**
— usuários agora só existem no banco, via `/api/usuarios/`.
**Limitação do ambiente onde essa rodada foi feita**: só havia Python 3.8 e
nenhum servidor PostgreSQL disponíveis, então não foi possível rodar
`makemigrations`/`migrate`/`runserver` nem testar o fluxo ponta a ponta
durante a implementação — o código foi revisado estaticamente com cuidado
(sintaxe, convenções do DRF, coerência entre serializers/views/frontend).
Rodar de verdade — incluindo gerar a migração inicial — fica para o
ambiente com Python 3.13 + Postgres 14.
### 14. Reorganização de pastas no padrão Django
O backend tinha sido criado numa subpasta `Portal/backend/`, com o
frontend (HTML/CSS/JS) solto na raiz de `Portal/` ao lado dela — funcional,
mas não era a estrutura convencional de um projeto Django. Pedido: "reorganize
a estrutura das pastas, conforme o padrão de um projeto Django". Mudanças:
- `manage.py`, `requirements.txt`, `.env`, `config/` e `portal_api/` subiram
de `Portal/backend/` para a raiz de `Portal/` — `Portal/` passou a **ser**
o projeto Django, não conter um subprojeto.
- As 5 páginas HTML foram movidas para `Portal/templates/` (antes soltas na
raiz) — é para lá que `TEMPLATES[0]["DIRS"]` aponta agora.
- `assets/css`, `assets/js` e `img/` viraram `static/css`, `static/js` e
`static/img` (pasta `assets/` removida) — `STATICFILES_DIRS` (novo em
`settings.py`) aponta pra lá, e `STATIC_ROOT` foi adicionado para
`collectstatic` em produção.
- Os 5 templates passaram a usar `{% load static %}` + `{% static 'css/x.css' %}`
em vez de caminhos hardcoded (`assets/css/x.css`) — forma idiomática do
Django de referenciar estáticos.
- `config/urls.py` perdeu os `re_path`/`static_serve` manuais para
`assets/`/`img/` — `django.contrib.staticfiles` já serve `static/`
automaticamente em `DEBUG` a partir de `STATICFILES_DIRS`, sem
configuração extra.
- Bug corrigido de passagem: o `.env` já existia (com as credenciais do
Postgres) mas `settings.py` nunca chamava `load_dotenv()` — ele nunca
tinha sido lido de verdade. Corrigido ao mexer em `settings.py` por causa
da mudança de `BASE_DIR`.
Nenhuma mudança de comportamento — só de organização de arquivos; os
endpoints da API, os models e a lógica do frontend continuam os mesmos da
rodada 13.
### 15. Dispensar notificações persiste por usuário
Bug relatado: ao clicar no X de uma notificação individual, o card do
dropdown fechava inteiro (comportamento incorreto) e, além disso, qualquer
notificação dispensada (X individual ou "Limpar tudo") voltava a aparecer
depois de um reload da página.
- **Fechamento indevido do dropdown**: causado pela ordem de eventos —
`list.innerHTML` era reconstruído (removendo o botão clicado do DOM) antes
do clique terminar de subir até o listener global em `document` que fecha
o dropdown em clique fora; como o botão já não estava mais na árvore,
`dropdown.contains(event.target)` avaliava falso, fechando o card como se
fosse clique externo. Corrigido com `event.stopPropagation()` no handler
de dismiss em `notifications.js`.
- **Notificação reaparecendo no reload**: era o comportamento documentado
até então (`notifications.js` guardava a lista só em memória, "zera a cada
reload") — o usuário pediu explicitamente para persistir no backend em vez
de usar `localStorage`. Criado o model `NotificacaoDispensada` (chave
natural `usuario` + `notif_id`, mesmo padrão de `Favorito`/`WidgetUsuario`)
e o endpoint `/api/notificacoes-dispensadas/`. `notifications.js` agora
busca os IDs já dispensados no carregamento e filtra a lista antes de
renderizar, e persiste via POST a cada X clicado ou "Limpar tudo".
Importante: só o **estado de dispensada** passou a ser real — o conteúdo
das notificações de "nova ferramenta" (`PID_NEW_TOOLS_NOTIFICATIONS`)
continua mockado/hardcoded no frontend, só os itens de compromisso vêm de
dados reais.
### 16. Links & Ferramentas — tela nova + permissão de edição dedicada
Pedido: estruturar de verdade a tela "Links & Ferramentas" (até então só um
item de menu com `href="#"`, sem página própria), com base num print da
ferramenta antiga (grade de cartões com logo/nome de sistemas externos —
Ottimizza, Sieg, GLPI, Trello, WhatsApp etc., cada um abrindo o link
correspondente). Três exigências específicas:
1. **Edição restrita por permissão de perfil**: só perfis com uma permissão
dedicada podem reordenar, incluir e remover os cartões — hoje só
habilitada para "Integração e Inovação" (`gerencia_links_ferramentas=True`
no seed), mas com o toggle já disponível em Perfis de Acesso pra liberar
outros perfis depois, sem precisar de código novo.
2. **Adicionar link**: modal com Nome, Link (URL) e "uma foto ou ícone" —
interpretado como upload de imagem real (não uma URL de ícone nem um
emoji/picker), já que o print de referência mostra logos de marca de cada
ferramenta.
3. Nada além disso foi pedido — não existe hoje edição de nome/URL/ícone de
um link já criado pela UI (só admin do Django), por decisão de manter o
escopo no que foi pedido (adicionar/remover/reordenar).
O que foi construído:
- **Nova permissão no mesmo padrão de `gerencia_permissoes`**: campo
`gerencia_links_ferramentas` (BooleanField) em `PerfilAcesso`, união em
`Usuario.gerencia_links_ferramentas()`, classe `PodeGerenciarLinksFerramentas`
em `permissions.py`, exposta em `me.gerencia_links_ferramentas`. É uma flag
**separada** do toggle de visibilidade do módulo "Links & Ferramentas" (que
já existia no catálogo, controla só se o item aparece no menu) — uma
controla quem *vê* a tela, a outra quem pode *editar o conteúdo* dela.
Adicionado um checkbox dedicado na aba Permissões de `perfis-acesso.html`
(`#pa-gerencia-links-ferramentas`), fora da árvore de módulos/apps — hoje é
o único toggle "especial" desse tipo com UI própria (`gerencia_permissoes`
em si só é setável via seed/admin, sem checkbox equivalente ainda; não
alterado nesta rodada por não ter sido pedido).
- **Model `LinkFerramenta`**: lista **global** (sem FK de usuário, ao
contrário de Favorito/WidgetUsuario) — todo mundo vê os mesmos cartões.
Campo `ordem` (inteiro) para a sequência de exibição; `icone` é `ImageField`
opcional (exigiu adicionar Pillow ao `requirements.txt` e configurar
`MEDIA_URL`/`MEDIA_ROOT`, que não existiam no projeto até agora — servidos
em `DEBUG` por `config/urls.py`, sem equivalente em produção ainda
configurado além do padrão whitenoise/nginx já usado pra `STATIC_ROOT`).
- **Reordenação sem endpoint de lote**: mover um cartão faz duas chamadas
PATCH trocando o `ordem` de dois itens adjacentes — decisão deliberada de
não criar um endpoint de bulk-reorder nem usar drag-and-drop (sem
biblioteca no projeto, drag-and-drop nativo teria custo de acessibilidade/
touch maior que o ganho); a UI usa duas setas (mover pra cima/baixo) por
cartão, visíveis só pra quem tem a permissão.
- **Upload multipart**: `pidApiRequest` (`api.js`) só sabia enviar JSON —
estendido para detectar `body instanceof FormData` e, nesse caso, deixar o
browser montar o `Content-Type: multipart/form-data` com boundary sozinho
(sem isso, o upload do ícone quebraria).
- Página nova `links-ferramentas.html`, réplica do shell padrão (mesma
sidebar/topbar dos outros 5 templates) + `links-ferramentas.js` +
`links-ferramentas.css`; item do menu "Links & Ferramentas" trocou de
`href="#"` pra `href="links-ferramentas.html"` nos 5 templates que
replicam a sidebar.
Mesma limitação de ambiente das rodadas anteriores: sem Python 3.13/Postgres
neste ambiente, não foi possível rodar `makemigrations`/`migrate` (novo
model + dois campos novos em `PerfilAcesso`) nem testar upload de imagem de
ponta a ponta.
### 17. Ambiente ganhou Python 3.13 + Postgres — migração da rodada 16 aplicada
O usuário avisou que "todas as ferramentas necessárias já estão instaladas"
neste ambiente. Confirmado: o `.venv` do projeto já tem Django 6.0.7, DRF
3.17.1, psycopg, python-dotenv e Pillow 12.3.0 (Python 3.13.14), e há um
Postgres local acessível pelas credenciais do `.env` — inclusive já com
migrações antigas aplicadas até `0002_notificacaodispensada` (rodada 15),
de uma sessão anterior fora deste histórico. A limitação documentada nas
rodadas 13–16 (só Python 3.8, sem Django, sem Postgres) não vale mais —
`CLAUDE.md` foi corrigido pra refletir isso.
Com o ambiente disponível, gerada e aplicada a migração pendente da rodada
16 (`0003_linkferramenta_and_more`: model `LinkFerramenta` + campo
`gerencia_links_ferramentas` em `PerfilAcesso`) e reaplicado `seed_portal`
— `gerencia_links_ferramentas=True` confirmado no perfil "Integração e
Inovação". Ainda não testado num navegador de verdade (login +
upload de ícone + reordenar + notificações persistindo entre reloads) —
próximo passo natural se o usuário quiser essa validação.
### 18. Bug visual: espaçamento inconsistente na sidebar
Reportado com print: itens do menu com estrela de favorito (qualquer
`<a class="nav-item">`/`.nav-subitem`, exceto "Principal") pareciam ter
espaçamento diferente dos itens sem estrela (toggles de grupo como
Auditorias/Solicitações, que são `<button>`, não `<a>`, e por isso nunca
ganham estrela via `pidCollectFavoritableApps`). Causa raiz: `.nav-item-row`
(o wrapper que `favorites.js` injeta em volta de um link favoritável pra
acomodar a estrela) não tinha `padding-right` nenhum, então o `.fav-toggle`
ficava encostado na borda direita da sidebar — enquanto o chevron dos
toggles de grupo mantinha os 12px de padding-right do próprio `.nav-item`.
Corrigido com `padding-right: var(--space-3)` em `.nav-item-row`
(`layout.css`), replicado nos dois estados responsivos (sidebar colapsada
no desktop, sidebar recolhida no mobile) pra não descentralizar o ícone
quando a estrela está escondida.
### 19. Permissão de Links & Ferramentas: de checkbox dedicado para visualizar/editar na árvore
A permissão `gerencia_links_ferramentas` (criada na rodada 16 como
`BooleanField` dedicado + checkbox solto no topo do card de edição de
perfil) foi **revertida** a pedido do usuário: "ao invés de ter uma caixa de
seleção acima, deixar uma opção onde marca a liberação para visualizar o
Links & Ferramentas, com subseleções entre visualizar e editar. Pois
teremos outras aplicações com a mesma funcionalidade." Ou seja: o pedido não
era só um ajuste de UI, era um pedido de **modelo de dados reutilizável**
pra qualquer módulo futuro que precise da mesma distinção visualizar/editar.
Novo desenho (documentado em detalhe em `CLAUDE.md` → "Padrão
visualizar/editar"): em vez de um campo dedicado por módulo, "visualizar" e
"editar" passaram a ser **dois `apps` normais** de `links-ferramentas` em
`catalogo.MODULE_APPS` — reaproveitando 100% a árvore de permissões
genérica que já existia (`profiles.js`), sem nenhum código de UI novo. Isso
também tornou o backend mais estrito: antes, `GET /api/links-ferramentas/`
era liberado a qualquer autenticado; agora exige `apps.visualizar`, e a
escrita exige `apps.editar` — ambos checados por uma única classe genérica
`PermissaoApp(module_key, app_key)` (`permissions.py`) reaproveitável por
qualquer módulo futuro com a mesma necessidade, sem precisar de subclasse
nova.
Removido nesta rodada: campo `gerencia_links_ferramentas` em `PerfilAcesso`
(migração `0004_remove_perfilacesso_gerencia_links_ferramentas`), método
`Usuario.gerencia_links_ferramentas()`, classe `PodeGerenciarLinksFerramentas`,
o campo em `PerfilAcessoSerializer`/`PerfilResumoSerializer`/`me_view`, e o
checkbox `#pa-gerencia-links-ferramentas` em `perfis-acesso.html`/`profiles.js`.
Pegadinha resolvida no `seed_portal.py`: como `links-ferramentas` está em
`BASE_KEYS` (habilitado pra todo perfil), `catalogo.permissions_from_keys()`
habilitaria **todos** os apps do módulo de uma vez — incluindo "editar" pra
todo mundo. Corrigido forçando `apps.editar = False` explicitamente pra
qualquer perfil que não seja "Integração e Inovação" (código 8), depois de
montar o dict de permissões. Confirmado via shell: os 7 perfis "normais"
saíram com `visualizar=True, editar=False`; só o código 8 saiu com ambos
`True`.
Migração `0004` gerada e aplicada no ambiente local (Python 3.13/Postgres
disponíveis desde a rodada 17); `manage.py check` limpo.
### 20. Logo com texto branco + sidebar maior
Dois bugs visuais de marca reportados com print: (1) a logo da tela de
login (`logo.png`) tem o texto "DePaula Contadores" em preto, ilegível
contra o fundo escuro do card de login (`--bg-surface`); (2) a logo da
sidebar (`logo-mono.png`, versão toda em branco/cinza claro) estava
pequena demais (`.sidebar__logo` era 56×56 fixo, espremendo uma arte que é
bem mais larga que alta) — pedido explícito pra usar ali uma versão com o
**D colorido e a letra branca**, num tamanho maior.
Essa variante (D colorido + texto branco) não existia como arquivo — só
existiam `logo.png` (D colorido, texto preto) e `logo-mono.png` (tudo
branco, D incluído). Gerada uma nova, `logo-branco.png`, com um script
Python usando Pillow (ambiente já tinha Pillow desde a rodada 16): varre
`logo.png` pixel a pixel e recolore pra branco só os opacos
quase-neutros/escuros (`max(r,g,b) < 70` e `spread(r,g,b) < 12`) — o
degradê dourado/marrom do D nunca bate nesse filtro porque mesmo na sombra
mais escura mantém um matiz quente (testado: pixel mais escuro do D
amostrado foi `(80,65,42)`, spread 38; texto era `(0,0,0)` puro, spread 0).
572.209 pixels alterados. Resultado confirmado visualmente (Read da imagem
gerada) antes de usar.
Trocado `logo.png` → `logo-branco.png` no login (`index.html`) e
`logo-mono.png` → `logo-branco.png` nos 5 shells (mesmo arquivo nos dois
lugares — ambos os fundos são escuros, então a mesma variante serve para
os dois). `.sidebar__logo` foi de `56×56` fixo pra `width:200px;
height:auto` (a arte é ~1.41:1, largura bem maior que altura — forçar
quadrado a esmagava); adicionado encolhimento pra `44px` nos dois estados
de sidebar colapsada (`.is-collapsed` no desktop, breakpoint mobile) pra
não vazar da faixa de 76px.
`logo.png` e `logo-mono.png` continuam no repo (não usados em nenhum
template agora) — o primeiro como fonte pra regerar `logo-branco.png` se a
arte oficial mudar, o segundo como variante alternativa disponível.
### 21. Ramais — tela nova
Pedido: reconstruir de verdade a tela "Ramais" (até então só um item de menu com `href="#"` e um botão "Acessar Ramais" sem destino em todos os shells), com base em prints da ferramenta antiga: diretório de ramais alimentado pelo cadastro real de usuário (Nome/Departamento/Ramal), modal de "Adicionar um Novo Ramal" (com opção de vincular um usuário existente ou criar uma linha avulsa "Não Tem Usuário"), botão "Novo Chamado", botão "Criar Ausência" e cartões especiais para colaborador ausente/aniversariante. Três decisões confirmadas com o usuário antes de implementar:
- **Novo Chamado** abre embutido na própria tela (modal com `<iframe>` apontando pro token da ferramenta de chamados), não em nova aba — diferente do padrão de todo outro link externo do portal.
- **Editar um ramal vinculado a usuário**: só o número do Ramal é gravável nesta tela — grava direto em `Usuario.ramal`; Nome/Departamento continuam só leitura do cadastro.
- **Ausência**: "(AUSENTE)" é calculado automaticamente comparando a hora atual com o período criado, mais uma ação de "Encerrar Ausência" pra quem volta antes do previsto.
- **Férias**: fora de escopo — o próprio usuário disse que depende de uma integração futura com outro banco; nenhuma UI de férias foi construída.
O que foi construído (detalhe técnico completo em `CLAUDE.md` → seção "Ramais"):
- Dois models novos: `Ramal` (linha da tela, opcionalmente vinculada a `Usuario` — vinculada, os dados vêm sempre do cadastro; avulsa, `nome`/`departamento`/`numero` moram na própria linha) e `RamalAusencia` (`esta_ativa()` calcula "ausente agora" comparando datas/horas, sem armazenar um flag; `encerrada_manualmente` permite encerrar antes do previsto sem mexer no período original).
- Mesmo padrão visualizar/editar da rodada 19 (Links & Ferramentas): `ramais` ganhou os dois apps na árvore de permissões, com o mesmo cuidado no `seed_portal.py` de forçar `editar=False` pra todo perfil que não seja Integração e Inovação (já que `ramais` está em `BASE_KEYS`, `permissions_from_keys()` habilitaria os dois de uma vez sem esse override).
- Endpoint dedicado `GET /api/ramais/usuarios/` pra alimentar o `<select>` "Lista de Usuários" dos modais, em vez de reaproveitar `/api/usuarios/` — aquele é restrito a `gerencia_permissoes`, e a permissão de Ramais foi mantida deliberadamente desacoplada disso (mesmo espírito da própria refatoração visualizar/editar da rodada 19).
- Página nova `ramais.html` + `ramais.js` + `ramais.css`, réplica do shell padrão, reaproveitando a tabela genérica `.pa-table*` de `perfis-acesso.css` (mesmo padrão que `usuarios.html` já usa sem CSS de tabela próprio). Nos outros 5 templates, o item de menu "Ramais" (antes `href="#"`) e o botão "Acessar Ramais" do topbar (antes um `<button>` sem handler nenhum) passaram a apontar de verdade pra `ramais.html`.
- Migração `0012_ramal_ramalausencia` gerada e aplicada no ambiente local; `seed_portal` reaplicado (confirmado via shell: 7 perfis com `ramais.apps.editar=False`, só "Integração e Inovação" com `True`).
Ainda não testado num navegador de verdade — próximo passo natural se o usuário quiser essa validação (ver "Roadmap" abaixo).
### 22. Ramais: colaboradores aparecem automaticamente, sem precisar "adicionar"
Depois de testar a rodada 21 no navegador (print anexado: só Gabriel aparecia na lista, porque ele tinha se "adicionado" manualmente pelo modal), o usuário pediu duas coisas:
1. "Pode trazer automaticamente o cadastro do usuário para esta tela de ramais, ficando apenas pendente o cadastro de ramal, se necessário" — ou seja, o modelo original (rodada 21), que exigia criar uma linha `Ramal` vinculada a um `Usuario` pra alguém aparecer no diretório, criava um passo manual desnecessário: todo colaborador com conta no Portal já deveria aparecer sozinho, com o ramal pendente até ser preenchido.
2. "Melhore a visualização da opção de adicionar ramal, o ícone de seleção está feio e destoa da página" — o `<select>` nativo do browser (sem nenhum CSS) destoava do tema escuro do resto do portal.
O que mudou:
- **`Ramal` perdeu o campo `usuario`** (migração `0013_remove_ramal_usuario_alter_ramal_nome`) — agora é só linha avulsa (sem conta de sistema por trás, ex.: telefone de sala), com `nome` obrigatório e `departamento`/`numero` opcionais (podem ficar pendentes, igual a um colaborador de verdade).
- **`RamalViewSet.list()` foi reescrito** pra mesclar duas fontes numa lista só, sem passar pelo `RamalSerializer`: todo `Usuario` ativo (linha montada direto do cadastro — `nome`, `departamentos`, `ramal`) + as linhas avulsas de `Ramal`. Cada item ganha um `id` sintético (`"usuario-<id>"`/`"avulso-<id>"`) e um `tipo`, que o frontend usa pra decidir a ação certa.
- **Editar o ramal de um colaborador de verdade** deixou de ser "criar uma linha vinculada" — agora é a nova action `PATCH /api/ramais/usuarios/{usuario_id}/` (`atualizar_ramal_usuario`), que grava direto em `Usuario.ramal`. "Adicionar Ramal" ficou só para linhas avulsas — o `<select>` "Lista de Usuários" que existia nesse modal foi **removido** (não faz mais sentido, já que o colaborador já aparece sozinho); só o modal de Criar Ausência ainda tem esse `<select>` (via `GET /api/ramais/usuarios/`, agora simplificado pra só `id`/`nome`).
- **Excluir** só existe pra linhas avulsas agora (não dá pra "excluir" um colaborador do diretório sem excluir a conta dele, o que é escopo da tela de Usuários) — o ícone de lixeira some das linhas de usuário de verdade.
- **Usuário inativo não aparece mais** no diretório (`list()` filtra `is_active=True`) — efeito colateral da reescrita que corrige uma limitação que a rodada 21 tinha deixado em aberto.
- **Select/textarea estilizados**: `components.css` ganhou `.modal-field select`/`.modal-field textarea` genéricos (seta customizada via `background-image`, sem o `appearance` nativo do browser) — não existia nenhum estilo pra esses dois elementos antes (só `input`), então qualquer modal futuro com `<select>`/`<textarea>` já sai consistente com o tema escuro sem precisar de CSS próprio.
Pegadinha resolvida durante a migração: como o campo `usuario` foi removido por completo (não só esvaziado), a única linha `Ramal` vinculada que já existia (a que o usuário tinha criado testando a rodada 21, ligada à própria conta dele) ficou órfã — virou uma linha avulsa com todos os campos em branco, porque a informação de qual usuário ela representava dependia só da FK removida. Identificada e apagada manualmente depois da migração (não tinha dado nenhum pra preservar, já que os três campos ficavam vazios pra linhas vinculadas no desenho antigo); o ramal do usuário de teste `bruno` também tinha sido alterado por um smoke test rodado antes desta correção e foi restaurado ao valor original. Sem consequência pra dado real, já que a tela nunca tinha sido usada em produção — mas fica registrado porque é o tipo de coisa que merece atenção se acontecer de novo com dado que importe.
## Limitações conhecidas / decisões assumidas
- Calendário De Paula (empresa) não tem mais versão interna — depende
inteiramente do link externo `depaula-tvcorporativa.lovable.app`.
- Widgets: só um tipo implementado (Calendário Individual) até agora.
- Sem teste automatizado (nem no frontend, nem no backend Django) — todo o
fluxo foi validado manualmente no navegador. As rodadas 13 e 14 não
puderam ser testadas ponta a ponta no ambiente da época; a partir da
rodada 17 o ambiente já roda `migrate`/`runserver` de verdade (ver acima),
mas o teste manual no navegador de Links & Ferramentas (rodada 16) e da
persistência de notificações (rodada 15) ainda não foi feito.
- Tema (claro/escuro e cor) continua só no `localStorage` — é preferência
de navegador, decisão deliberada de manter fora da migração para o
banco.
- Links & Ferramentas: não dá pra editar nome/URL/ícone de um link já
criado pela UI, só adicionar/remover/reordenar (o que foi pedido na
rodada 16) — editar um existente hoje é só via Django admin.
- Ramais: sem UI de férias (integração futura com outro banco, combinado
com o usuário na rodada 21); excluir um colaborador do diretório não é
possível por ali (só linhas avulsas podem ser excluídas) — é escopo da
tela de Usuários, por decisão da rodada 22.
- Simulação de Custo de Contratação (rodada 37): v1 cobre só Empregado
CLT — as outras 4 modalidades do pedido original ficam para quando
houver uma planilha/regra de referência validada pelo contador.
- Indicador de Desempenho (rodada 38): v1 cobre só o Fiscontábil — outros
departamentos ficam para rodada futura; sem visão própria do
colaborador no Portal (recibo é documento interno do RH).
### 23. Ramais: ajustes visuais + visualizar/editar/excluir ausência
Três pedidos pequenos, feitos em sequência depois de testar a rodada 22 no navegador:
1. **Overflow no modal de Adicionar Ramal**: `.modal-field-row` usava `grid-template-columns: 1fr 1fr` sem `minmax(0, ...)`, e os inputs de `.modal-field` não tinham `width: 100%` explícito — o conteúdo (tamanho intrínseco do `<input>`) empurrava a grade além da borda do modal. Corrigido em `components.css` (`grid-template-columns: minmax(0, 1fr) minmax(0, 1fr)` + `width: 100%` em `input`/`select`/`textarea` de `.modal-field`) — bug genérico, vale pra qualquer modal do portal com campos lado a lado, não só Ramais.
2. **Modal "Novo Chamado" cortando conteúdo**: a ferramenta externa embutida no iframe (rodada 21) é mais alta que um modal padrão; o iframe tinha altura fixa (`min(70vh, 640px)`) menor que o conteúdo, cortando o formulário no meio de um campo em vez de rolar de forma previsível. Corrigido em `ramais.css`: `.ram-chamado-card` passou a ocupar quase a tela inteira (`min(960px, 95vw)` × `min(92vh, ...)`, o teto em px ajustado depois pelo próprio usuário direto no CSS) e o iframe virou `flex:1` dentro do `.modal-card` (que já é flex-column), preenchendo todo o espaço vertical restante.
3. **Visualizar/editar/excluir ausência**: o usuário passou um print de como devia ficar — clicar no badge "(AUSENTE)" de uma linha abre um modal "Visualizar Ausência" com os campos do período (desabilitados) e dois botões, "Editar Ausência" e "Deletar Ausência". Implementado reaproveitando o `RamalAusenciaViewSet` que já existia (nenhuma mudança de backend — `GET`/`PATCH`/`DELETE` em `/api/ramais-ausencias/{id}/` já funcionavam, só faltava a UI): o badge virou um `<button data-ram-ver-ausencia>` que busca o registro via `GET` e abre o modal; "Editar" habilita os mesmos campos in-place (sem modal novo) e troca os botões por "Cancelar"/"Salvar Ausência"; "Deletar" remove o registro de verdade. Isso **substituiu** o botão "Encerrar Ausência" da rodada 21 (que só marcava `encerrada_manualmente=True`, preservando o registro) — o campo continua no model, mas sem UI própria; editar/excluir cobre o caso de uso real de forma mais direta, do jeito que foi demonstrado.
Nenhuma migração nova (mudança 3 não tocou o backend). `manage.py check` limpo depois das mudanças 1 e 2 (não alteraram Python, só confirmado por precaução).
### 24. Ramais: contraste dos indicadores de ausente/aniversariante
Prints em claro e escuro mostrando as linhas de Bruno (ausente) e Willian (aniversariante) — pedido pra melhorar a legibilidade. Perguntado antes de mexer, porque "observações" no pedido original não batia com nenhum print (nenhum mostrava o campo Observações do modal de Ausência); o usuário confirmou que o problema era **o tingimento de fundo da linha inteira** (`.ram-row--ausente td`/`.ram-row--aniversario td`, rodada 21) somado à cor do próprio selo — duas camadas de cor translúcida da mesma matiz empilhadas, contraste ruim nos dois temas.
Primeira tentativa: removido o tingimento da linha inteira, deixando só o selo comunicar o status (mesmo padrão do selo "Pendente"). O usuário pediu de volta o tingimento — "quero que deixe o tingimento, porém que fique num tom mais forte, para facilitar sua visualização". Desenho final: `.ram-row--ausente`/`.ram-row--aniversario` td voltaram (com opacidade maior que a versão original da rodada 21) **e** `.ram-badge--ausente` virou um selo (pill) de verdade, igual ao de aniversariante — as duas coisas juntas, cada uma com cor de texto/fundo ajustada por tema via `:root[data-theme="light"]` (mesmo padrão de override que `tokens.css` já usa) em vez de reaproveitar `--danger`/`--gold` crus, que não tinham contraste suficiente pensados pra texto pequeno sobre um selo ou fundo de linha.
### 25. Favicon
Pedido: "no ícone da aba do navegador, deve constar esta logo minimalista do escritório" — o usuário passou um print de referência: só o D (o mesmo pen/quill em degradê dourado/marrom de `logo.png`), sem o texto "De Paula Contadores", sobre fundo transparente.
Esse recorte isolado do D não existia como arquivo — as três variantes de `static/img/` sempre têm o texto junto. Gerado `favicon.png` a partir de `logo.png` reaproveitando o mesmo filtro criado na rodada 20 pra `logo-branco.png` (pixel opaco quase-neutro/escuro = texto, nunca o D, que mantém matiz quente até na sombra mais escura do degradê) — mas em vez de recolorir esses pixels pra branco, apagados (`alpha=0`) pra isolar só o D; resultado recortado pelo bounding box do que sobrou e centralizado num canvas quadrado transparente (o D é mais alto que largo), redimensionado pra 192×192. Resultado comparado visualmente (`Read` da imagem gerada) contra o print do usuário antes de usar — bateu.
Ligado via `<link rel="icon" type="image/png" href="{% static 'img/favicon.png' %}">` no `<head>` das 7 páginas (`index.html` + os 5 shells + `ramais.html`). Nenhuma mudança de Python; `manage.py check` limpo por precaução.
### 26. Bug: busca de aplicações deixava grupos do menu abertos depois de limpar
Reportado com print: depois de pesquisar em "Pesquise por Aplicação..." (`search.js`) e apagar a busca, os grupos do menu (Portais, Geradoc etc.) que a busca tinha aberto pra mostrar o resultado continuavam abertos, em vez de voltar ao estado de antes de buscar.
Causa: `search.js` só tinha o caminho de **abrir** um `nav-group` (`classList.add("is-open")`) quando ele dava match durante a digitação — nunca fechava de volta quando o termo era apagado. Corrigido guardando, no início de cada sessão de busca (primeira tecla digitada), quais grupos já estavam abertos manualmente (`gruposAbertosAntesDaBusca`, um `Set`); ao limpar o campo, cada grupo volta exatamente pro estado daquele snapshot, em vez de simplesmente remover `is-open` de todo mundo (o que fecharia até um grupo que o usuário tinha aberto manualmente antes de buscar). Só front-end (`static/js/search.js`), nenhuma mudança de backend.
### 27. Botão "Criar Ausência" fora do padrão + texto do menu no tema claro
Dois ajustes pequenos:
- **Botão "Criar Ausência"** (`ramais.html`) usava `btn-outline`, destoando dos outros dois botões da mesma barra de ações ("Adicionar Ramal", "Novo Chamado"), ambos `btn-solid`. Trocado pra `btn-solid`, sem nada mais mudar no comportamento.
- **Texto do menu no tema claro**: a sidebar é intencionalmente congelada (fundo sempre escuro nos dois temas, ver `CLAUDE.md` → "CSS — organização entre arquivos"), mas as variáveis de texto (`--sidebar-text-primary`/`--sidebar-text-secondary`/`--sidebar-text-muted`) nunca tinham sido sobrescritas pro tema claro — pedido explícito pra virarem branco puro nesse tema, pra melhorar a legibilidade contra o fundo que continua escuro. Adicionada uma exceção deliberada em `tokens.css` (`:root[data-theme="light"]`) só pra essas três variáveis; fundo/borda da sidebar continuam frozen como antes. `CLAUDE.md` atualizado pra documentar a exceção, já que a regra geral (não usar overrides de tema na sidebar) continua valendo pro resto.
### 28. Fonte "Bree Serif" no título da tela inicial
Pedido: trocar a fonte do texto "Portal De Paula" no topbar da tela inicial (`portal.html`) pra Bree Serif, serif, negrito — só ali, não em `--font-sans` (usada em todo o resto do portal) nem no `.topbar__title` dos outros shells (que mostram outros títulos, como "Ramais"/"Administração").
Primeira fonte externa do projeto — até aqui só fontes do sistema (`--font-sans`). Carregada via Google Fonts (`<link rel="preconnect">` + `<link ... family=Bree+Serif>`) no `<head>` de `portal.html`. Estilo aplicado num id novo, `#portal-title` (no mesmo `<h1 class="topbar__title">` de sempre, só ganhou o id), colocado em `widgets.css` por ser o único CSS próprio da página — mesmo não sendo um widget. Ajustado por iterações diretas do usuário logo em seguida: negrito removido, tamanho aumentado (1.3rem), negrito devolvido, negrito removido de novo — estado final é Bree Serif 400/1.3rem. Na mesma leva de ajustes, o usuário pediu a mesma fonte também no título "Portal De Paula" da tela de login (`.login-card__title`, `login.css`) — `index.html` ganhou o mesmo `<link>` do Google Fonts.
### 29. Animações — pedido explícito pra ignorar `prefers-reduced-motion`
Pedido: "inclua animações nas páginas, para deixá-las fluidas", com duas condições explícitas — rápidas ("otimizando o tempo") e que **ignorem** a preferência de acessibilidade do sistema operacional/navegador do usuário (`prefers-reduced-motion`). Isso é uma inversão deliberada da prática padrão de acessibilidade (que normalmente desativaria animação quando essa preferência está ativa) — decisão do dono do produto pro próprio portal interno, registrada aqui pra não ser "corrigida" de volta sem confirmar de novo.
Implementado com três `@keyframes` genéricos em `base.css` (`pidFadeIn`, `pidFadeSlideUp`, `pidScaleIn` — único CSS carregado por toda página, inclusive `index.html`), aplicados via `animation` (não `transition`, que não anima a troca `display:none` ↔ visível causada por `hidden`) em: `.modal-overlay`/`.modal-card` (todo modal do app), `.account-dropdown`/`.notif-dropdown`, `.page-content` (toda navegação entre páginas) e `.login-card`. Botões (`.btn-solid`/`.btn-outline`/`.btn-ghost`/`.btn-danger-outline`/`.icon-btn`) ganharam `transform: scale()` no `:active` como feedback de clique, e `.pa-table td` (reaproveitada por Perfis de Acesso/Usuários/Ramais) ganhou `transition: background` no hover da linha, que antes trocava sem transição nenhuma. Durações todas curtas (120–200ms, a maioria usando os tokens `--transition-fast`/`--transition-base` que já existiam). Nenhum `@media (prefers-reduced-motion: reduce)` foi adicionado — ausência deliberada, não descuido (ver `CLAUDE.md` → "Animações").
### 30. "Acessar Ramais" vira modal de consulta rápida, em vez de navegar
Pedido, com print de referência do portal antigo: o botão "Acessar Ramais" do topbar (presente em `portal.html`, `links-ferramentas.html` e `calendario-individual.html` — só essas 3 páginas têm o atalho) não deveria mais levar pra `ramais.html`; deveria abrir um modal com a lista de ramais cadastrados, com busca por nome/departamento/ramal. O print de referência mostrava um estilo DataTables (branco/azul, "Show N entries", colunas ordenáveis, paginação numerada) — decisão de modernizar a funcionalidade pro visual escuro/roxo do resto do portal, não clonar pixel a pixel (mesmo critério já usado desde a rodada 1).
O que foi construído:
- `#ramais-btn` voltou a ser um `<button>` (não `<a href="ramais.html">`, que era o estado desde a rodada 21) nas 3 páginas.
- Modal novo (`#ramais-lookup-modal`, duplicado nas 3 páginas — mesmo padrão de outros modais compartilhados como o de senha) + `static/js/ramais-lookup.js` + `static/css/ramais-lookup.css`: busca numa caixa só (filtra as três colunas ao mesmo tempo, mais simples que a busca em duas caixas da tela `ramais.html`), ordenação por coluna (clique no cabeçalho alterna asc/desc) e paginação (10/25/50/100 por página) — tudo client-side, sem endpoint novo, reaproveitando `GET /api/ramais/` (a mesma listagem mesclada usuário+avulso da tela completa), buscado uma vez por abertura de página.
- Gate por permissão: o botão some (`hidden`) se o perfil do usuário não tiver `apps.visualizar` em `ramais` — mesmo padrão do resto do app.
- Botão "Ir para Controle de Ramais" no rodapé do modal é o link de verdade pra `ramais.html` (tela com edição); o modal em si é só consulta.
- Pegadinha resolvida: a tabela do modal não podia reaproveitar `.pa-table` de `perfis-acesso.css` porque `portal.html`/`links-ferramentas.html`/`calendario-individual.html` não carregam esse CSS (só `ramais.html`/`usuarios.html`/`perfis-acesso.html` carregam) — `ramais-lookup.css` ficou com estilo de tabela autocontido (`.ram-lookup-table*`) em vez de depender de um CSS que a página não tem.
Nenhuma mudança de backend — `GET /api/ramais/` já existia e já retornava exatamente os dados necessários. `manage.py check` limpo (confirmado por precaução, já que a mudança foi só front-end).
### 31. Ramais vira uma seção com 5 subtelas (abas)
Dois prints do sistema antigo mostraram que "Ramais" no fundo é uma seção com 5 subtelas navegáveis por abas: Ramais, Responsável no Tareffa, Telefones Externos, Férias, Funções de Telefonia. Pedido: adicionar essa navegação em `ramais.html`, construir Telefones Externos e Funções de Telefonia de verdade, e criar Responsável no Tareffa/Férias como abas vazias ("sem informações, pois serão trabalhadas posteriormente"). Isso **supera** a decisão da rodada 21 de que Férias estava fora de escopo — agora a aba existe (vazia); a limitação de fundo (integração futura com outro banco) continua valendo, só a navegação foi antecipada.
Perguntado antes de implementar se "Funções de Telefonia" (tabela de comandos tipo `*01 + Código de Agente` → LogOn) deveria ser conteúdo fixo no HTML ou uma lista administrável no banco — o usuário escolheu **lista administrável (CRUD completo)**, mesmo padrão de Telefones Externos.
O que foi construído:
- **Dois models novos**, ambos sem FK pra `Usuario` (dados avulsos, mesmo espírito de `Ramal` avulso): `TelefoneExterno` (nome/ramal/telefone/observações, só `nome` obrigatório) e `FuncaoTelefonia` (comando/função/resumo, só `comando` obrigatório). `FuncaoTelefonia.Meta.ordering = ["comando"]` reproduz sozinho a ordem do print (`*0, *01, *02, *03, *1, *2, *20, *21, *22, *23, *5, *503, *8`) porque essa é exatamente a ordem lexicográfica da string — dispensou um campo `ordem` manual e endpoint de reorder.
- **Permissão reaproveitada**: as duas subtelas usam o mesmo par `ramais.apps.visualizar`/`ramais.apps.editar` que já existia (`PermissaoApp("ramais", app_key)`) — são subtelas da mesma seção do menu, não aplicações novas; nenhuma mudança em `catalogo.py`.
- **Seed só para Funções de Telefonia**: `seed_portal.py` ganhou `FUNCOES_TELEFONIA_SEED` (as 13 linhas do print, via `update_or_create` por `comando`, idempotente) — decisão de que são comandos padrão de central telefônica (documentação genérica), diferente de Telefones Externos, que começa **vazio** de propósito (são contatos reais de fornecedores/terceiros, o usuário cadastra pela própria tela).
- **Abas**: reaproveitado o CSS genérico `.pa-tabs`/`.pa-tab`/`.pa-tab-panel` que já existia em `perfis-acesso.css` (mesmo padrão das abas Permissões/Usuários de `perfis-acesso.html`), com uma implementação independente em `ramais.js` (`data-ram-tab`/`data-ram-tab-panel`/`activeRamTab`) pra não colidir com `profiles.js`. O conteúdo que já existia (filtros/tabela/botões de Ramais) migrou pro painel `data-ram-tab-panel="ramais"`, sem mudar de comportamento.
- Endpoints novos: `/api/telefones-externos/` e `/api/funcoes-telefonia/`, CRUD padrão via `ModelViewSet`.
Migração `0014_funcaotelefonia_telefoneexterno` gerada e aplicada no ambiente local; `seed_portal` reexecutado (confirmado via shell: 13 linhas de Funções de Telefonia, na ordem certa). `manage.py check` limpo. Ainda não testado num navegador de verdade — cadastrar/editar/excluir um Telefone Externo e uma Função de Telefonia, alternar entre as 5 abas, e conferir que um perfil só-visualizar não vê nenhum botão de escrita fica para a próxima validação.
### 32. Ramais: permissão granular por subtela (visualizar por aba + editar nas 3 administráveis)
No mesmo dia da rodada 31, o usuário pediu um ajuste no modelo de permissão recém-criado: em vez de um único par `visualizar`/`editar` cobrindo as 5 abas de Ramais, cada aba deveria ter sua própria permissão de visualização, e só as 3 com conteúdo administrável (Ramais, Telefones Externos, Funções de Telefonia) deveriam ter também uma permissão de edição própria — Responsável no Tareffa/Férias, sendo placeholders vazios, só precisam de visualizar. Pedido explícito de "estado inicial": só "Integração e Inovação" com acesso de editar nas 3, os demais 7 perfis com visualizar liberado em todas as 5.
Implementado reestruturando `catalogo.MODULE_APPS["ramais"]` de uma lista flat de 2 apps pra **5 subgrupos** (mesmo formato `{"key", "label", "tools": [...]}` já usado em Auditorias) — cada subgrupo é uma aba, com 1 tool (`Visualizar`) ou 2 (`Visualizar`/`Editar`). Isso reaproveitou 100% a árvore de permissões genérica de `profiles.js` (`renderTree`/`renderEntry`/`renderLeaf` já sabem renderizar subgrupos com `tools`, usado desde a rodada de Auditorias) — nenhum código de UI novo em Perfis de Acesso.
O que mudou:
- **Backend**: os 4 `ModelViewSet` relacionados (`RamalViewSet`, `RamalAusenciaViewSet`, `TelefoneExternoViewSet`, `FuncaoTelefoniaViewSet`) passaram a checar a chave específica da própria subtela (`"telefones-externos-visualizar"`/`"editar"`, etc.) em vez do genérico `"visualizar"`/`"editar"` — `RamalAusenciaViewSet` usa as mesmas chaves de `ramais-visualizar`/`ramais-editar` do diretório, por ser parte dessa mesma aba.
- **`seed_portal.py`**: o override que força `editar=False` pros 7 perfis "normais" cresceu de 1 chave (`ramais.apps.editar`) pra 3 (`ramais-editar`, `telefones-externos-editar`, `funcoes-telefonia-editar`) — mesmo princípio de sempre, só mais chaves.
- **`ramais.js`**: reescrito o bloco de permissão — de um único `canView`/`canManage` pra 5 pares (um por aba), um objeto `ramTabViewPerms` que esconde (`hidden`) o botão de cada aba sem `visualizar`, e a aba ativa por padrão passou a ser a primeira visível (não sempre "Ramais", que pode estar oculta pra um perfil específico no futuro, embora hoje todos os 7 perfis "normais" vejam as 5).
- **`ramais-lookup.js`** (modal de consulta rápida no topbar): trocado de `apps.visualizar` genérico pra `apps["ramais-visualizar"]` especificamente, já que esse modal só mostra o diretório de Ramais.
Como isso mudou as **chaves** armazenadas em `PerfilAcesso.permissoes["ramais"]["apps"]` (não só valores), foi necessário reexecutar `seed_portal` pra sincronizar os 8 perfis com o novo formato — sem isso, todo perfil ficaria sem nenhum acesso a Ramais (as chaves antigas `visualizar`/`editar` não existem mais no catálogo). Confirmado via shell depois do reseed: os 7 perfis "normais" saíram com as 5 chaves `*-visualizar=True` e as 3 `*-editar=False`; só "Integração e Inovação" saiu com as 3 `*-editar=True` também. `gabriel`/`bruno` não foram tocados (proteção da rodada anterior, [[feedback_seed_nao_reseta_gabriel_bruno]] segue valendo). `manage.py check` limpo.
**Nota pra quem for customizar um perfil manualmente na tela de Perfis de Acesso**: qualquer ajuste fino que já tivesse sido feito nas chaves antigas (`ramais.visualizar`/`ramais.editar`) precisa ser refeito nas novas chaves — é uma troca de namespace, não uma migração automática de valor (JSONField não tem esse mecanismo).
### 33. Liderança (gerente/coordenador)
Pedido: gerentes/coordenadores precisam enxergar a agenda de compromissos privados de quem lideram, sem precisar que cada liderado marque o compromisso como "todos"/"departamento". Implementado com `Usuario.lideranca` (booleano) + `Usuario.liderados` (M2M auto-referenciado, `symmetrical=False` — "A lidera B" não implica o contrário). Dois pontos gravam a mesma relação: a tela de Usuários (seção "Liderança" no formulário, só quem tem `gerencia_permissoes`) e um modal novo "Gerenciar Usuário" (substituiu o antigo botão direto "Alterar senha" no dropdown da conta), que permite ao próprio gerente se autogerenciar via `PATCH /api/me/liderados/` sem precisar de acesso à tela administrativa. `CompromissoAgendaViewSet.get_queryset` passou a incluir compromissos `"somente_eu"` de quem está em `usuario.liderados`, mas sem dar direito de editar (`sou_dono` continua `False` pra esses). O checklist de liderados (com busca por nome/departamento e "marcar todos os resultados da busca") virou um componente genérico (`components.css`) reaproveitado nos dois lugares. Migração `0015_usuario_liderados_usuario_lideranca`. Detalhe completo em `CLAUDE.md` → "Liderança (gerente/coordenador) e o modal 'Gerenciar Usuário'".
### 34. Acessos Gerais — segunda aplicação de Links & Ferramentas
Pedido, com uma tela do Asana como referência: um cadastro de acessos/logins compartilhados da equipe (ex.: login geral de um site), organizado em seções e linhas, com popup de detalhes por linha. Virou a segunda aplicação real da seção "Links & Ferramentas" — o item do menu, que antes era um link direto, passou a ser um `nav-group` expansível com dois sub-itens ("Links & Ferramentas" e "Acessos Gerais"), cada um favoritável separadamente.
O que foi construído: dois models novos (`AcessoGeralSecao`, `AcessoGeral`, sem relação com `LinkFerramenta`), mesmo padrão visualizar/editar já usado em Links & Ferramentas (chaves próprias `acessos-gerais-visualizar`/`acessos-gerais-editar`); ordenação escopada por seção (não global); restrição opcional de seção por perfil (`perfis_restritos` M2M pra `PerfilAcesso` — filtro de **dado**, independente da árvore de permissões); e um editor de "Observações" com texto rico + imagens embutidas (`contenteditable`, colar/arrastar imagem vira `data:` URI, sem upload separado). Sanitização no backend via `nh3` (não `bleach`, sem manutenção desde 2023) — allowlist estrita de tags/atributos, permitindo só `<img>` com esquema `data:` além de tags de texto básicas, contra XSS via HTML malicioso injetado no payload. Migrações `0016_acessogeralsecao_acessogeral` e `0017_acessogeralsecao_perfis_restritos_and_more`. Detalhe completo em `CLAUDE.md` → "Acessos Gerais".
### 35. Eventos Corporativos no Calendário Individual
Pedido: distinguir compromissos pessoais/departamentais de **eventos corporativos** formais (reuniões, treinamentos) — com categoria, local, modalidade (presencial/remoto/híbrido) e descrição — e restringir quem pode criar compromissos visíveis para o departamento ou para todos, que até então qualquer usuário podia fazer livremente.
O que foi construído: `CategoriaEvento` (nome único + cor hex, cadastro editável direto no modal de criar/editar compromisso, botão "+" ao lado do `<select>` de categoria); `CompromissoAgenda` ganhou `categoria` (FK), `eh_evento` (booleano — força `visibilidade="todos"` na validação), `local`, `modalidade` e `descricao`. Nova permissão `calendario-individual-criar-evento` (`catalogo.py`, único `tool` dentro de um subgrupo — não um par visualizar/editar, já que o módulo em si é liberado a todo perfil) passou a ser exigida por `CompromissoAgendaSerializer.validate()` para qualquer compromisso com `visibilidade` em `departamento`/`todos` — liberada só pra "Integração e Inovação" no `seed_portal.py` por ora, mesmo cuidado de sempre (`calendario-individual` está em `BASE_KEYS`). No calendário, eventos ganharam uma 5ª categoria de filtro/cor (`"evento"`, token `--coral`), distinta das 4 já existentes (`somente_eu`/`departamento`/`todos`/`equipe`). Migrações `0018_remove_compromissoagenda_compartilhado_com_perfil_and_more`, `0019_categoriaevento_compromissoagenda_descricao_and_more` e `0020_compromissoagenda_eh_evento`. Detalhe completo em `CLAUDE.md` → "Eventos Corporativos" (dentro de "Calendário Individual e Widgets").
### 36. Importação de Plano de Saúde (Utilitários)
Primeira aplicação de "Utilitários" (os placeholders "Conversor de Arquivos"/"Calculadora Fiscal" foram removidos do menu nesta mesma rodada). Importa o relatório de faturamento de uma operadora de plano de saúde/odontológico (Amil, Unimed) e gera o arquivo de lançamento no leiaute do Questor, mais um relatório de auditoria do que não pôde ser lançado automaticamente. A lógica de negócio veio de um pipeline já testado (`projects/importacao-planos-saude.skill` + protótipo em `projects/project/`), portado quase 1:1 pra dentro do Django em `portal_api/planos_saude/` (pacote Python puro, sem ORM).
Decisões principais: permissão de **toggle único** (quem tem acesso faz o fluxo inteiro — criar, revisar, gerar); histórico completo (cada importação fica salva, diferente de um fluxo descartável); regra de custeio (empresa/empregado/específica, com limite de valor e/ou percentual) configurável por tipo de lançamento e tipo de beneficiário na tela, em vez da regra fixa do pipeline original; resolução manual de auditoria por nome (confirmação humana item por item quando o casamento automático falha, nunca fuzzy matching); pré-validação de cada arquivo anexado antes do envio final, reaproveitando o mesmo parser Python que o `create()` usaria. Migrações `0021_importacaoplanosaude_importacaoplanosaudeauditoria_and_more` e `0022_importacaoplanosaudeauditoria_resolucao_manual`. Detalhe completo em `CLAUDE.md` → "Importação de Plano de Saúde (Utilitários)".
### 37. Simulação de Custo de Contratação (Geradoc)
Pedido (2026-08-11): substituir a planilha manual de custo de contratação (`projects/planilha de custo/*.xlsx`) por um formulário no Portal que gera um PDF pronto pra enviar ao cliente — primeira aplicação de "Geradoc" a sair do estado de placeholder.
Decisões confirmadas com o usuário: v1 cobre só Empregado CLT (as outras 4 modalidades do pedido original — Simples Nacional, Regime Normal, Pró-labore, Empregado Doméstico — ficam para quando houver planilha de referência equivalente); cálculo pontual, sem persistência (devolve o PDF direto, nada salva no banco); corrigido um gap real da planilha original (dedução por dependente não somava à base do IRRF quando usava desconto real de INSS); e as tabelas de INSS/IRRF, que nasceram hardcoded, viraram um cadastro editável (`ParametroFiscalCustoContratacao`, singleton) na mesma rodada em que a redução de IRRF da Lei 15.270/2025 (vigente desde jan/2026) foi implementada, já que ambas mudam por lei/todo ano. PDF gerado via `reportlab` (adicionado ao `requirements.txt`), com cabeçalho nas cores reais da marca (dourado/marrom amostrados de `logo.png`), não o roxo do tema do Portal. Migração `0023_parametrofiscalcustocontratacao`. Detalhe completo em `CLAUDE.md` → "Simulação de Custo de Contratação (Geradoc)".
### 38. Indicador de Desempenho (Geradoc)
Pedido (2026-08-12): substituir a apuração manual do indicador de desempenho do Fiscontábil — feita numa planilha (`projects/Indicadores/FISCO CONTABIL *.ods`) com fórmulas quebradas por edições manuais acumuladas — por uma ferramenta completa dentro de Geradoc, com histórico de apurações mensais e recibo em PDF por colaborador. A maior feature construída até aqui em número de models/endpoints (6 models, 6 `ModelViewSet`, pacote de negócio próprio `portal_api/indicadores/` com 6 arquivos).
Escopo confirmado com o usuário: v1 cobre só o Fiscontábil (papéis Balancete/Liberação Fiscal/Conciliação); o "tipo" do colaborador é derivado por empresa via a planilha "Serviços Tareffa", não é cadastro; só 3 critérios (entrega de balancetes/liberações fiscais/conciliações no prazo) são calculados automaticamente, todo o resto é marcação manual do RH; critérios e percentuais por tipo são cadastros genéricos editáveis pela tela, não hardcoded (percentuais nunca editados in-place, só um histórico com `vigente_desde`); ajuste manual em dois níveis (por critério, e pelo percentual agregado Individual/Grupo/Departamento, este último aplicado de uma vez a todo o grupo/departamento — "cada gerente representa um grupo"); recibo é documento interno do RH, sem visão do próprio colaborador nesta v1. Validado com dados reais de 43 colaboradores, o que revelou e corrigiu dois bugs de robustez: `openpyxl` em modo `read_only` precisa de `.close()` explícito no Windows (senão bloqueia excluir o upload depois) e campos percentuais precisaram de `max_digits=7` (não 6) pra não estourar em cálculos que batem exatamente 100%. Primeiro histórico populado via `seed_indicador_desempenho` (idempotente), com os valores exatos da planilha antiga. Migrações `0024` a `0028`. Detalhe completo em `CLAUDE.md` → "Indicador de Desempenho (Geradoc)".
Ajuste pequeno feito logo depois de testar a tela de revisão no navegador: a coluna "Meta (%)" da tabela "Metas de Grupo e Departamento" era um campo de texto livre (permitindo qualquer percentual) — o usuário apontou que, pra Grupo/Departamento, só existem duas possibilidades reais ("será pago ou não"), então o campo virou um `<select>` Sim/Não (100%/0%), reaproveitando o mesmo raciocínio já aplicado aos critérios individuais de Grupo/Departamento (que também só têm Sim/Não, sem "não se aplica"/"não faz"). O percentual Individual de cada colaborador (composição ponderada dos 3 níveis) continua livre, por poder ser legitimamente fracionário.
Segundo ajuste, também depois de testar no navegador: o resumo do card de cada colaborador só mostrava "Individual: X%", sem explicar como esse número foi composto. Pedido: mostrar a composição completa (ex.: "Individual: 57,14% (peso 60%) Grupo: 100% (peso 10%) Departamento: 100% (peso 30%) Total Indicador: 74,29%"), com cada um dos 3 níveis em verde/vermelho conforme foi atingido (100%) ou não; o Total em si fica neutro (sem cor), pra não repetir a mesma informação 4 vezes. O percentual bruto de Individual (antes da composição) e o peso de cada nível eram calculados em `calculo.recalcula_colaborador` mas descartados depois de usados — extraída a lógica de filtro de respostas por nível pra uma função reaproveitável (`respostas_aplicaveis`) e adicionada `composicao_individual()`, exposta como campo computado (`composicao_individual`) em `IndicadorApuracaoColaboradorSerializer`, sem nenhuma migração (não persiste nada novo, só reconstrói pra exibição). Aproveitado pra adicionar `prefetch_related` na action `retrieve` de `IndicadorApuracaoViewSet` (`colaboradores__empresas`, `colaboradores__respostas__criterio`), já que o campo novo faria mais uma consulta por colaborador na tela de revisão sem isso. Layout do cabeçalho do card também foi reorganizado a pedido do usuário: a composição saiu de baixo do nome/gerente pra ficar ao lado, centralizada, numa coluna própria do grid; e o lápis de ajuste manual do Total passou a ficar sempre ao lado do valor (isolado numa linha própria que nunca quebra), não mais embaixo quando o rótulo "Total Indicador" (mais longo que o antigo "Individual") não coubesse na coluna.
Terceiro ajuste: a tabela de Metas de Grupo/Departamento tinha um `<select>` Sim/Não duplicado por critério — um ao lado do texto que descreve o critério (bulk, via `aplicar-em-lote`) e outro na coluna "Meta (%)" à direita (que já ajusta `pct_grupo`/`pct_departamento` direto). O usuário pediu pra remover o primeiro, mantendo só o da direita — `renderMetaCriteriosHtml` voltou a ser só texto informativo (nome + peso do critério), e o listener de `change` associado a `.ind-meta-criterio-select` foi removido. Responder um critério específico continua possível por colaborador, dentro do card de revisão (`renderRespostasGrupoHtml`) — só o atalho de responder em lote pela tabela de metas deixou de existir.
Quarto ajuste: o modal "Ajuste Indicador em Lote" (ajusta `pct_individual` de vários colaboradores selecionados de uma vez) tinha um campo de texto livre "Percentual Individual (0 a 100)". Mesmo raciocínio das rodadas anteriores — o RH só usa esse ajuste em lote pra dois casos reais ("considerar atingido, mesmo quem não bateu a meta" ou "desfazer o ajuste manual") — trocado por dois botões, "Sim" (aplica `pct_individual=100` a todos os selecionados) e "Reverter" (chama a action `recalcular` de cada colaborador selecionado, voltando ao cálculo automático). Nenhuma mudança de backend — os dois endpoints por-colaborador já existiam (PATCH e `recalcular`), só o disparo em paralelo (`Promise.all`) mudou de "um valor pra todos" pra "uma ação pra todos". Ajustado de novo logo em seguida: "Reverter" e "Sim" (renomeado pra "Ajustar") viraram os dois `btn-solid`, mesma cor — só "Cancelar" ficou `btn-outline` — já que as duas ações são igualmente "reais", não uma primária/secundária.
Quinto ajuste: o checkbox "Só com honorário não encontrado" (filtrava a lista de colaboradores pra só quem tinha alguma empresa sem honorário) virou um botão dedicado, "Visualizar Empresas sem Honorário" (com contador), que abre um modal próprio. Pedido explícito do usuário: a mesma empresa pode aparecer sob mais de um colaborador (um responsável pelo balancete, outro pela liberação fiscal, outro pela conciliação financeira) — preencher o honorário uma vez deve valer pra todos eles de uma vez, já que o honorário é da empresa, não da pessoa. Isso não era possível antes: o campo de preencher honorário existente (`PATCH /api/indicadores-apuracoes-empresas/{id}/`) só ajustava uma linha por id.
O que foi construído: o modal agrupa as `IndicadorApuracaoEmpresa` com `honorario_nao_encontrado=True` da apuração por `codigo_empresa` (mostrando os colaboradores/tipos responsáveis por cada uma), com um campo de honorário por grupo. Novo endpoint `POST /api/indicadores-apuracoes/{id}/ajustar-honorario-empresa/` (`IndicadorApuracaoViewSet.ajustar_honorario_empresa`, serializer `IndicadorApuracaoAjusteHonorarioEmpresaSerializer`) atualiza **todas** as linhas com aquele código na apuração de uma vez, recalculando cada colaborador afetado — mesmo padrão de `ajustar_grupo`/`ajustar_departamento` (aplicar uma mudança a um escopo de uma vez, não registro a registro). O endpoint por-linha antigo continua existindo, usado pela tabela "Empresas" de dentro do card do colaborador (caso raro de querer corrigir só uma linha). Nenhuma migração — só um endpoint novo, sem campo novo no model.
Bug corrigido logo depois de testar: numa apuração com muitas empresas sem honorário, o modal crescia além da altura da tela (mesma causa raiz já documentada em `CLAUDE.md` pra `.calendar-day` — um filho de `flex-column` só rola em vez de esticar o pai quando o próprio pai também tem uma altura limitada e o filho tem `min-height:0`). Corrigido dando `max-height:85vh` a `.ind-esh-modal-card` e `overflow-y:auto`/`min-height:0` à lista (`.ind-esh-list`) — título, aviso e os botões de ação ficam sempre visíveis, só a lista de empresas rola internamente quando não cabe.
Sexto ajuste, três pedidos numa rodada só: (1) ao preencher o honorário (linha única ou em lote pelo modal novo), deixar uma nota "honorário ajustado manualmente" na tabela "Empresas" de dentro do card do colaborador — campo novo `IndicadorApuracaoEmpresa.honorario_ajustado_manualmente` (migração `0029`, junto com a mudança do item 2), marcado pelos dois caminhos de ajuste (linha única e em lote) e exposto no serializer; sem UI de reverter, já que não existe "automático" pra essa linha voltar (o código nunca casou com a planilha). (2) Listar as empresas por código, não por nome, na mesma tabela — trocado `IndicadorApuracaoEmpresa.Meta.ordering` de `["nome_empresa", "id"]` pra `["codigo_empresa", "id"]` (mesma migração `0029`). Bug reportado logo depois de testar: "80" e "503" apareciam no fim da lista, depois de "2134" — `codigo_empresa` é `CharField`, então ordenar só por ele é alfabético (`'8'`/`'5'` são "maiores" que `'1'`/`'2'` como caractere, mesmo o número sendo menor), não numérico. Corrigido (migração `0030`) ordenando primeiro por `Length("codigo_empresa")` e só depois pelo valor — pra códigos sem zero à esquerda, string mais curta é sempre número menor, então isso reproduz a ordem numérica certa sem precisar converter pra inteiro (que quebraria com erro de banco se algum código não fosse só dígitos). (3) Botão "Visualizar Empresas sem Honorário" ganhou cor de atenção (`--danger`, mesma linguagem visual do input/selo de honorário não encontrado) — não reaproveitado `.btn-danger-outline` (que tem `margin-right:auto`, pensado pra separar um botão "Excluir" dentro de `.modal-actions`, efeito colateral indesejado no toolbar) — classe própria `.ind-empresas-sem-honorario-btn` só com as cores.
Sétimo ajuste, testando o modal "Empresas sem Honorário" com dados reais: três pedidos. (1) Ordenar também por código nessa lista (estava só por nome) — reaproveitado o mesmo critério "tamanho da string primeiro" da correção anterior, agora também em JS (`empresasAgrupadasPorCodigo`), já que essa lista é montada em memória a partir do que já foi carregado, não vem de uma query com `Meta.ordering`. (2) Código antes do nome no cabeçalho de cada item — trocada a ordem dos dois `<span>` (`.ind-esh-codigo` primeiro), mesma ordem da tabela "Empresas" do colaborador (coluna "Código" antes de "Empresa"). (3) Um segundo botão/modal, "Verificar Empresas Ajustadas Manualmente", pra rever e **corrigir** um honorário já ajustado — antes só dava pra preencher uma vez (o modal "sem honorário" some da lista assim que `honorario_nao_encontrado` vira falso, sem nenhum caminho de volta pra editar de novo). Backend: o filtro de `ajustar_honorario_empresa` mudou de `honorario_nao_encontrado=True` pra `Q(honorario_nao_encontrado=True) | Q(honorario_ajustado_manualmente=True)` — o mesmo endpoint agora cobre preenchimento inicial e correção, sem endpoint novo. Frontend: as funções de agrupar/renderizar/salvar dos dois modais foram generalizadas (parametrizadas por um filtro e pelos ids de cada um) em vez de duplicadas; o modal de correção pré-preenche o campo com o valor atual (o de preenchimento inicial continua em branco).
Ajustado de novo na sequência, testando o segundo botão/modal recém-criado: o usuário pediu pra **não** ter um botão separado — "facilitando a usabilidade da ferramenta". Revertido pra um popup só: o botão/modal "Verificar Empresas Ajustadas Manualmente" foi removido, e sua lista virou uma segunda seção dentro do próprio popup "Empresas sem Honorário" (`.ind-esh-section-title` como divisor), embaixo da lista original. As duas listas passaram a viver num wrapper único que rola (`.ind-esh-scroll`), com o título e o botão "Fechar" sempre visíveis fora dele — antes cada modal tinha sua própria rolagem. `renderEmpresasHonorario()` (nova função) renderiza as duas listas de uma vez, tanto ao abrir o popup quanto depois de qualquer "Salvar" — necessário porque corrigir uma empresa "sem honorário" faz ela migrar pra seção "ajustada manualmente" na hora, então as duas sempre precisam refletir o estado atual juntas. Nenhuma mudança de backend nesta correção.
Ajustado uma terceira vez, testando a versão com as duas seções sempre visíveis: pedido pra a seção "ajustadas manualmente" ficar escondida por padrão, atrás de um botão no final do modal — "lá seja possível a correção" quando o usuário quiser ver. Adicionado `#ind-empresas-ajustadas-toggle-btn` (largura cheia, com contador, alterna "Visualizar"/"Ocultar Empresas Ajustadas Manualmente (N)") logo depois da lista principal, escondendo `#ind-empresas-ajustadas-section` por padrão (`hidden`, resetada a cada abertura do popup) e só renderizando/mostrando a lista quando o botão é clicado. `renderEmpresasHonorario()` ajustada pra só re-renderizar a seção "ajustadas" se ela já estiver aberta — evita trabalho à toa quando ela está escondida, mas mantém sincronizada se o usuário já estiver com ela visível ao salvar algo na lista principal. Nenhuma mudança de backend.
### 39. Indicador de Desempenho: checklist de validação por colaborador
Pedido: um checkbox no início de cada card de colaborador (tela de revisão), pra o RH marcar quem já validou — ao marcar, a borda do card fica verde, pra dar visibilidade de quem ainda está pendente numa apuração com muitos colaboradores.
O que foi construído: campo novo `IndicadorApuracaoColaborador.validado` (migração `0031`) — booleano simples, sem relação com nenhum cálculo (nem participa de `calculo.recalcula_colaborador`). Novo endpoint `POST /api/indicadores-apuracoes-colaboradores/{id}/marcar-validado/` só grava esse campo. Diferente de todos os outros ajustes desta tela (que recarregam a apuração inteira e re-renderizam tudo depois de qualquer mudança), marcar/desmarcar o checklist atualiza só o card clicado no DOM, sem recarregar nem re-renderizar a lista inteira — decisão deliberada, já que essa ação tende a ser repetida muitas vezes seguidas numa conferência longa, e um refresh completo fecharia outros cards já expandidos e resetaria a posição de rolagem a cada clique. Erro de rede reverte o checkbox e o estado local, mesmo padrão de outros toggles imediatos do app.
Detalhe de acessibilidade descoberto ao implementar: o gate de clique que expande/recolhe o card no cabeçalho precisou excluir o `<label>` inteiro do checkbox, não só o `<input>` — clicar na área do label fora do glifo do checkbox dispara dois eventos de clique encadeados (um no label, outro sintético no input), e só excluir o input pelo seletor deixava o primeiro clique (target=label) passar batido, expandindo/recolhendo o card ao mesmo tempo que marcava/desmarcava o validado.
### 40. Indicador de Desempenho: corrigir o responsável por uma empresa/papel
Pedido, com exemplo concreto: a empresa 168 (MULTIVERSA CONSULTORIA LTDA) tinha "Valéria Bonete — Fiscal" como responsável, mas devia ser "Alan Lima Cassulli — Fiscal". Ou seja, reatribuir qual colaborador responde por um papel (tipo) de uma empresa — diferente de tudo que já existia na tela (honorário, percentuais), que nunca mexia em **quem** é o responsável, só em valores.
O que foi construído: novo botão "Corrigir Responsável" (popup próprio, busca por nome/código entre **todas** as empresas da apuração — não só as com honorário pendente) — selecionar uma empresa mostra a mesma lista de responsáveis já usada no popup "Empresas sem Honorário" ("Nome — Tipo"), mas agora com um `<select>` por linha pra escolher outro colaborador da apuração. Novo endpoint `POST /api/indicadores-apuracoes-empresas/{id}/trocar-responsavel/` (`IndicadorApuracaoEmpresaViewSet.trocar_responsavel`) só troca a FK `colaborador` da linha (`codigo_empresa`/`tipo`/honorário continuam intactos) e recalcula os **dois** colaboradores envolvidos — o que perdeu a empresa e o que ganhou, já que o conjunto de empresas de cada um mudou. Validações no backend: o novo colaborador precisa existir na mesma apuração, não pode ser o mesmo de já é, e não pode já ser responsável por essa mesma empresa/tipo (evitaria duas linhas duplicadas). O `<select>` de cada linha já exclui o colaborador atual das opções e nasce num placeholder desabilitado, pra nunca reatribuir sem escolha explícita do usuário. Nenhuma migração — só um endpoint novo, reaproveitando o model existente.
Ajuste pequeno na sequência, no botão "Visualizar Empresas sem Honorário" (popup "Empresas sem Honorário", rodada anterior): ele continuava vermelho e com o mesmo texto mesmo quando não havia mais nenhuma empresa pendente. Pedido: nesse caso, o botão devia virar "Visualizar Empresas com Honorário Ajustado" e perder a cor de atenção, já que não há mais nada de errado pra resolver. `atualizarBotaoEmpresasSemHonorario()` (JS) passou a alternar a classe `.ind-empresas-sem-honorario-btn` (antes fixa no HTML, agora só aplicada via JS quando `semHonorario > 0`) e o texto do botão conforme o total de empresas pendentes.
### 41. Indicador de Desempenho: filtro por setor organizacional, com meta de Departamento própria por setor
Pedido, olhando a tabela "Metas de Grupo e Departamento" real (competência 2026-07, 43 colaboradores): trocar o filtro por gerente por botões de departamento/setor no topo da revisão — selecionar um setor (ex.: "Fisco/Contábil") devia restringir a tabela de Metas a só a linha de Departamento daquele setor + os gerentes dele, e a lista de colaboradores abaixo a só quem tem esses gerentes. O pedido também trazia uma regra de negócio concreta: "Contabilidade" e "Fiscal / Tributário" formam um setor só ("Fisco/Contábil"); os demais setores são independentes; os próprios líderes do Fisco/Contábil (João Candido Rodrigues, Lhais Vergilio Delavy, Paloma Ramão, Elizangela dos Santos) deviam aparecer num setor à parte, "Gerentes"; e Luciane Gonzaga no setor "Rocket".
Antes de implementar, esclarecido com o usuário (a mudança tinha implicações que iam além de "só um filtro visual", ver perguntas feitas): (1) cada setor passaria a ter sua própria meta de Departamento (`pct_departamento`), não mais um valor único pra toda a apuração — escolhido em vez de manter uma meta global só filtrada na tela; (2) a associação pessoa→setor devia ser um cadastro editável, não fixo no código — mas o usuário revelou, na sequência, que o setor de cada colaborador **já existe** como coluna ("departamento") na planilha Serviços Tareffa, então não precisava de um cadastro manual pra maioria dos casos.
Investigado o arquivo real (`projects/Indicadores/Serviços Tareffa.xlsx`) pra confirmar: a coluna "departamento" tem valores "Contabilidade"/"Fiscal / Tributário"/"Rocket"/"Condomínio"/"Pessoa Física IRPF"/"Auditoria Fisco/Contábil" por linha de serviço. Rodando a extração real dos 43 colaboradores da apuração existente, confirmou-se que os 4 líderes citados pelo usuário têm o setor bruto da planilha igual ao setor operacional de quem lideram (ex.: João Candido Rodrigues aparece como "Contabilidade", não "Gerentes") — ou seja, a fusão automática Contabilidade/Fiscal→Fisco/Contábil não bastava pra colocá-los em "Gerentes"; só Luciane Gonzaga já vinha certa ("Rocket") sem precisar de nada extra.
**O que foi construído** (ver CLAUDE.md → "Setor organizacional" pro detalhe técnico completo):
- Campo novo `IndicadorApuracaoColaborador.setor` (migração `0032`) — valor bruto da coluna "departamento" da planilha, lido em `leiaute.le_servicos_tareffa` (campo novo `LinhaTareffa.setor`) e propagado por `pipeline.processa_apuracao` até o colaborador (primeira ocorrência por responsável, mesmo padrão já usado pra `gerente`).
- Model novo `IndicadorSetorApelido` (nome do colaborador → setor) — cadastro persistente e editável (aba "Apelidos de Setor" em Configurações, `/api/indicadores-setores-apelidos/`, CRUD completo) pros casos em que o setor bruto da planilha não reflete o setor "de verdade" da pessoa (os 4 líderes). Cadastrados no ambiente local: os 4 nomes → "Gerentes".
- `portal_api/indicadores/setores.py` (novo módulo, sem ORM direto além de ler `IndicadorSetorApelido`): `resolve_setor(nome, setor_bruto, apelidos)` aplica o apelido se existir, senão funde "Contabilidade"/"Fiscal / Tributário" em "Fisco/Contábil" (`FUSAO_SETORES`) e usa o valor bruto como está pros demais (Rocket, Condomínio, Pessoa Física IRPF, ...) — nenhum código novo é necessário quando um setor novo aparecer na planilha.
- `pct_departamento` deixou de ser ajustado/recalculado pra **toda** a apuração de uma vez — `IndicadorApuracaoViewSet.ajustar_departamento`/`recalcular_departamento` agora recebem `setor` no corpo e aplicam só aos colaboradores daquele setor (`setores.colaboradores_do_setor`), mesma mecânica que `ajustar_grupo`/`recalcular_grupo` já usavam por gerente.
- Frontend (`indicador-desempenho.js`): removido o `<select id="ind-filtro-gerente">`; adicionados botões de setor (`#ind-filtro-setor`, `.ind-setor-chip`) no topo da seção de Metas — clicar num setor filtra tanto a tabela de Metas (uma linha "Departamento (todo o &lt;setor&gt;)" + as linhas de Grupo só dos gerentes daquele setor) quanto a lista de colaboradores abaixo. Campo `setor_bucket` (`SerializerMethodField`, já resolvido no servidor) exposto por colaborador.
- **Apurações criadas antes desta mudança** ficam com `setor` em branco — feito um backfill pontual, via `manage.py shell`, lendo de novo a planilha Tareffa já anexada à única apuração existente no ambiente (competência 2026-07); não existe management command dedicado pra isso ainda, caso surja uma apuração antiga sem o arquivo disponível.
Validado contra os dados reais da apuração existente antes de considerar pronto: sem apelido, o agrupamento automático já dava Fisco/Contábil=35 (incluindo os 4 líderes) + Condomínio=4 + Pessoa Física IRPF=1 + Rocket=2 + Auditoria Fisco/Contábil=1; com os 4 apelidos cadastrados, o resultado bateu exatamente com o pedido: Fisco/Contábil=32 (gerentes "João Candido Rodrigues"/"Lhais Vergilio Delavy"), Gerentes=4, Rocket=2, Condomínio=4, Pessoa Física IRPF=1.
### 42. Indicador de Desempenho: três ajustes rápidos de usabilidade na tela de revisão
Testando a rodada 41 no navegador, três pedidos pequenos em sequência:
1. **Info no "Honorário Ajustado"**: pedido, com print, de um botão de informação ao lado do cabeçalho da coluna "Honorário Ajustado" (tabela "Empresas" de dentro do card do colaborador) — ao clicar, abre um popup pequeno explicando que o valor é o honorário proporcionalizado ao percentual do Indicador atingido, já mostrando o percentual real daquele colaborador (não um texto genérico). Não existia nenhum componente de popover/tooltip no projeto — construído do zero (`.ind-info-wrap`/`.ind-info-btn`/`.ind-info-popover` em `indicador-desempenho.css`), inspirado no mesmo mecanismo de `.notif-dropdown` (`notifications.js`): botão "i" (`data-ind-info-toggle`) alterna um `<span>` posicionado em `absolute` logo abaixo, e um listener em `document` fecha ao clicar fora. Como o botão fica dentro de um `<th>` (que tem `text-transform: uppercase`/fonte pequena via `.pa-table th`), o popover precisou resetar essas propriedades pra virar texto normal de novo. Todo o texto (incluindo o percentual) é montado em `renderHonorarioAjustadoInfoHtml(colaborador)`, chamada por colaborador ao montar a tabela — sem mudança de backend, `pct_individual` já vinha no payload.
2. **Filtro por setor no "Ajuste Indicador em Lote"**: o modal já tinha busca por nome; pedido pra também poder filtrar por departamento/setor. Adicionado um `<select id="ind-lote-global-setor">` (opções = setores distintos da apuração, via `pidIndAgruparPorSetor`) que combina (E lógico) com a busca por nome — os dois filtros juntos decidem quem aparece no checklist e quem "Marcar todos os resultados da busca" marca. Nenhuma mudança de backend (filtragem 100% em memória sobre `colaborador.setor_bucket`, já exposto desde a rodada 41).
3. **Modal de seleção pra "Gerar Recibos"**: antes, o botão gerava na hora pra **todos** os colaboradores da apuração, sem escolha. Pedido: abrir um modal parecido com o de "Ajuste Indicador em Lote" — mesma busca por nome + filtro por setor + checklist com "marcar todos os resultados da busca" — permitindo gerar recibo de um colaborador só, de alguns específicos, de um setor inteiro, ou de todo mundo. Implementado reaproveitando exatamente o mesmo padrão de UI do item 2 (funções/variáveis próprias, prefixo `gerarRecibos*`, sem duplicar `loteGlobal*`), com uma diferença deliberada: o checklist já nasce com **todo mundo marcado** ao abrir (reproduz o comportamento antigo — gerar pra todos — sem exigir que o RH marque um por um; ele só desmarca quem não quer incluir desta vez).
Mudança de backend: `IndicadorApuracaoViewSet.gerar` passou a aceitar `colaborador_ids` (lista, opcional) no corpo — sem isso, gera pra todos (comportamento antigo); com isso, gera só pra quem foi pedido, validando que todos os ids pertencem à apuração (400 caso contrário). Decisão tomada sem perguntar, por ser a leitura mais correta do dado: a apuração só é marcada `concluida` quando a seleção pedida cobre **todos** os colaboradores (sem filtro, ou uma seleção que bate com o total) — gerar um recibo avulso pra conferência não deveria fechar a apuração inteira como se o mês tivesse sido todo revisado. Validado via `django.test.Client` logado como `gabriel`: geração parcial (1 colaborador) manteve `status="revisao"`; geração de todos marcou `concluida`; um id de outra apuração devolveu 400. O estado real da apuração no ambiente local (já estava `concluida` de um uso anterior) foi restaurado ao original depois do teste.
### 43. Indicador de Desempenho: três ajustes no PDF do recibo
Pedido com print real do PDF gerado (colaborador com o Individual ajustado manualmente pra 100%, e colaborador Natan da Costa com a tabela de Empresas ilegível):
1. **Banner "PERCENTUAL DO INDICADOR INDIVIDUAL" mostrava o valor pago, não o medido**: quando o RH/Diretoria ajusta o percentual Individual manualmente (`pct_individual_ajustado_manualmente=True`), `colaborador.pct_individual` passa a guardar só o valor sobrescrito — o percentual real medido (a composição dos 3 níveis) não ficava salvo em lugar nenhum depois do ajuste. Pedido: o banner deve sempre mostrar o valor **efetivo/medido** (ex.: 74,29%), independente do ajuste; embaixo, a linha que dizia "Percentual individual ajustado manualmente pelo RH" devia virar "Percentual Individual Ajustado Pela Direção" **com o percentual ajustado ao lado** (ex.: 100%) — os dois números lado a lado, pra ficar claro o que foi atingido e o que foi considerado.
Implementado sem duplicar a fórmula: `calculo.composicao_individual()` (já calculava os 3 níveis brutos pra exibição no card de revisão) ganhou uma chave nova, `total_calculado` — a mesma composição ponderada (`_combina_niveis`) que `recalcula_colaborador` teria gravado em `pct_individual` se não houvesse ajuste manual, calculada ali mesmo a partir dos níveis que a função já monta. `recibo.py._banner_percentual` passou a mostrar `composicao["total_calculado"]` no banner principal (sempre o medido) e, só quando `pct_individual_ajustado_manualmente`, uma linha extra com o rótulo novo + `colaborador.pct_individual` (o valor pago).
2. **"Total Resultado" → "Total do Indicador"**: troca simples de rótulo na última linha da tabela "Empresas" (`_tabela_empresas`).
3. **Overflow generalizado na tabela "Empresas"** (causa raiz do "texto da coluna Tipo tapando o Hon. Ajustado" e dos valores da linha de total "ultrapassando as linhas"): toda célula da tabela, exceto "Empresa", era uma **string solta**, não um `Paragraph` — string solta não quebra linha dentro da largura da coluna; combinado com `ALIGN` à direita (aplicado a todas as colunas de valor, inclusive "Tipo" sem querer) ou ao negrito da linha de total (mais largo que a mesma string em peso normal), um valor mais largo que a coluna vazava visualmente por cima da célula vizinha em vez de quebrar linha — daí "Contador (com conciliador)" (right-aligned, mais largo que a coluna de Tipo) vazar pra esquerda cobrindo Hon. Ajustado, e os totais em negrito vazarem na última linha mesmo cabendo em peso normal. Corrigido convertendo toda célula em `Paragraph` com um estilo de alinhamento próprio (`celula_centro`/`celula_direita`/`celula_negrito`/`celula_direita_negrito`, novos em `_estilos()`) — agora qualquer valor mais largo que a coluna quebra em duas linhas em vez de vazar. Coluna "Tipo" também ganhou um dicionário de labels curtos só pro PDF (`TIPO_LABEL_CURTO`: "Contador SC"/"Contador CC" em vez de "Contador (sem/com conciliador)", abreviação que já era usada informalmente na documentação do projeto) — o label completo não cabia nem quebrando linha numa coluna estreita. Larguras de coluna também redistribuídas (Tipo de 1,5 pra 2,2cm, usando os ~1,3cm de folga que a tabela tinha sobre a largura útil da página A4).
Validado gerando de verdade os PDFs de "Alan Lima Cassulli" (sem ajuste manual — banner mostra só o medido, sem linha extra) e "Natan da Costa" (com ajuste manual pra 100% — banner mostra 81,74% medido + linha "Percentual Individual Ajustado Pela Direção: 100,00%") a partir da apuração real do ambiente, lendo o PDF gerado de volta pra conferir visualmente — tabela "Empresas" sem nenhum vazamento em nenhum dos dois casos, "Contador CC"/"Contador SC" legíveis na coluna Tipo, "Total do Indicador" com os 4 valores certos sem sobrepor.
### 44. Indicador de Desempenho: recibo em PDF — "R$" separando do valor + capitalização do rótulo
Testando a rodada 43 com valores maiores (ex.: R$ 27.710,19), dois ajustes finos:
1. **"R$" quebrando pra uma linha acima do valor**: ao converter as células da tabela "Empresas" pra `Paragraph` (rodada 43), o espaço comum entre "R$" e o número virou um ponto de quebra de linha válido — quando o valor não cabia numa linha só, o reportlab quebrava bem ali, deixando "R$" sozinho acima do número em vez de vazar (o bug da rodada anterior), mas ainda longe do ideal. Pedido: "R$" deve estar sempre do lado esquerdo do valor, nunca acima. Trocado o espaço comum por `&nbsp;` (não separável) em `_moeda()` — sozinho isso só moveu o ponto de quebra pra dentro do próprio número (ex.: "R$ 27.710,1" / "9"), então a correção completa também exigiu abrir mais espaço de verdade pras colunas: `LEFTPADDING`/`RIGHTPADDING` da tabela reduzidos de 6pt (padrão do reportlab) pra 3pt, e as larguras das colunas Honorário/Hon. Ajustado/Indiv./Grupo/Depto./Total redistribuídas (tirando um pouco de Código e Empresa, que tinham folga de sobra) pra caber o maior valor real visto na apuração (R$ 28.023,16) numa linha só, com padding.
2. **Capitalização do rótulo**: "Percentual Individual Ajustado Pela Direção" (Title Case) virou "Percentual individual ajustado pela direção" (só a primeira letra maiúscula).
Validado regerando os mesmos dois recibos da rodada 43 (Alan Lima Cassulli, Natan da Costa) — inclusive o valor mais alto da apuração inteira (R$ 28.023,16, CELLSHOP DUTY FREE no recibo de Natan da Costa) coube numa linha só, "R$" sempre grudado à esquerda do número, em toda a tabela e na linha de total.
### 45. Indicador de Desempenho: Departamento como entidade própria — critérios e percentuais por departamento
Pedido: "vamos passar a apurar as metas e regras por departamento" — critérios e percentuais deixam de ser globais (uma regra só pra toda a apuração) e passam a ser por departamento (ex.: a regra do Fisco/Contábil pode ser diferente da do Condomínio). Duas exigências explícitas: (1) precisa de um cadastro de verdade de departamentos, com relação a gerentes — um departamento pode ter mais de um gerente (ex.: Fisco/Contábil tem João Candido Rodrigues **e** Lhais Vergilio Delavy); por enquanto essa relação é mantida manualmente pela própria aplicação (alimentar da planilha fica pra decidir depois); (2) por enquanto replicar a mesma regra em todo departamento, mas a estrutura já precisa suportar customização.
**Decisão tomada com o usuário antes de implementar** (rodada consultada via pergunta direta, dado o tamanho da mudança): o novo cadastro de Departamento **substitui por completo** o mecanismo de "setor" da rodada 41 (coluna bruta da planilha + fusão automática + `IndicadorSetorApelido`) — fica só um conceito de departamento no sistema, usado tanto pras Metas quanto agora pra critérios/percentuais. O caso que o apelido resolvia (os 4 líderes de Fisco/Contábil) passa a ser coberto mapeando a gerente deles, "Elizangela de Paula Kuhn", pro departamento "Gerentes".
**O que foi construído** (ver CLAUDE.md → "Departamento organizacional" pro detalhe técnico completo):
- Dois models novos: `IndicadorDepartamento` (nome/ativo) e `IndicadorDepartamentoGerente` (gerente→departamento, `nome_gerente` único — um gerente só pertence a um departamento, mas um departamento aceita vários gerentes).
- `IndicadorCriterio`/`IndicadorPercentualTipo` ganharam FK obrigatória `departamento` — cada departamento passa a ter seu próprio histórico de critérios/percentuais, de verdade (não é mais um bucket calculado, é uma tabela filtrada por FK).
- `IndicadorApuracaoColaborador.setor` (texto) virou `departamento` (FK nullable) — resolvido uma vez na criação da apuração, a partir do `gerente` do colaborador via `IndicadorDepartamentoGerente` (não mais da coluna bruta da planilha). Sem mapeamento pro gerente, o colaborador fica com `departamento=None` e vira um aviso no processamento.
- `pipeline.processa_apuracao()` também passou a agrupar os **critérios automáticos** por departamento (`criterios_automaticos_por_departamento`) — cada colaborador só calcula os critérios do **seu** departamento, não mais todos os critérios ativos da apuração.
- Migração em 3 passos (schema com FK nullable → `RunPython` criando "Fisco/Contábil" e apontando todo critério/percentual já existente pra ele, já que era literalmente o que a regra única representava → schema tornando a FK obrigatória) — mesmo padrão de qualquer FK NOT NULL adicionada numa tabela já populada.
- `seed_indicador_desempenho.py` ajustado pra criar/reaproveitar o departamento "Fisco/Contábil" antes de popular critérios/percentuais (senão quebraria ao rodar de novo, já que os models agora exigem departamento) — testado rodando de novo: atualizou os 7 critérios e reconheceu os 5 percentuais já existentes, sem duplicar nada.
- Tela de Configurações: aba "Apelidos de Setor" virou aba "Departamentos" (cadastro de departamentos + botão "Gerenciar Gerentes" por linha, popup com lista de gerentes daquele departamento + campo pra adicionar um novo). Abas "Critérios" e "Percentuais por Tipo" ganharam um filtro por departamento no topo + coluna "Departamento" na tabela + campo obrigatório de departamento no modal de adicionar. Filtro de Metas/"Ajuste Indicador em Lote"/"Gerar Recibos" (todos já existentes desde a rodada 41) tiveram a chave de agrupamento trocada de string (`setor_bucket`) pra id (`departamento`), mesma mecânica visual.
Validado via `django.test.Client` (login `gabriel`): criados 4 departamentos de teste + 6 relações gerente→departamento reproduzindo a estrutura real conhecida (Fisco/Contábil: João Candido Rodrigues + Lhais Vergilio Delavy; Rocket: Luciane Gonzaga; Condomínio: Cristiano Silverio; Pessoa Física IRPF: Daniel Gustavo Manenti; Gerentes: Elizangela de Paula Kuhn) e gerada uma apuração de teste de verdade com os 2 arquivos reais — distribuição resultante: Fisco/Contábil 32, Gerentes 5, Rocket 1, Condomínio 4, Pessoa Física IRPF 1 (só os colaboradores de Fisco/Contábil ganharam respostas de critério, já que só esse departamento tem critérios cadastrados — esperado). Tudo (apuração de teste, departamentos de teste, relações de teste) removido depois do teste, mantendo só o "Fisco/Contábil" real criado pela migração.
**Limitação real encontrada nesse teste, documentada em CLAUDE.md**: resolver o departamento pelo `gerente` (não mais pelo colaborador individual) quebra o caso de uma gerente que supervisiona pessoas de departamentos diferentes — "Elizangela de Paula Kuhn" supervisiona os líderes de Fisco/Contábil **e** Luciane Gonzaga (que deveria cair em "Rocket", não em "Gerentes" junto com os outros). Como não existe mais uma exceção por colaborador individual (o antigo `IndicadorSetorApelido` cobria isso), Luciane Gonzaga passou a cair em "Gerentes" nesse teste — diferente do que a rodada 41 tinha estabelecido pra ela (Rocket). Sinalizado ao usuário como limitação conhecida do novo desenho; não corrigido nesta rodada por não ter sido pedido, e porque reintroduzir uma exceção por colaborador contrariaria a decisão de "só um mecanismo" tomada no início desta rodada — só mexer nisso se o usuário confirmar que quer.
### 46. Indicador de Desempenho: sugestões de gerente no popup "Gerenciar Gerentes"
Pedido, testando a rodada 45: o campo "Adicionar gerente" era só texto livre — o usuário pediu pra já vir preenchido com os nomes de gerente encontrados na última apuração, pra só precisar relacionar (clicar) em vez de redigitar cada nome (risco real de typo, já que o nome precisa bater exatamente com a coluna "gerente" da planilha pra apuração futura casar com o departamento certo).
Implementado 100% no frontend, sem endpoint novo: `carregarGerentesSugeridos()` busca a apuração mais recente (`GET /api/indicadores-apuracoes/`, a lista já vem ordenada por `-competencia`/`-criado_em` via `IndicadorApuracao.Meta.ordering` — não precisou de parâmetro novo), pega o detalhe dela (`GET /api/indicadores-apuracoes/{id}/`) e extrai os nomes distintos de `colaborador.gerente`, excluindo os que já estão em `departamentoGerentesConfig` (já mapeados pra algum departamento). O popup "Gerenciar Gerentes" ganhou uma seção "Sugestões" entre a lista atual e o campo de texto — cada sugestão é um botão (`.checklist-item` reaproveitado como `<button>`, mesmo padrão já usado em "Corrigir Responsável"/`.ind-corrigir-resultado`) que, ao ser clicado, já chama `POST /api/indicadores-departamentos-gerentes/` pra aquele departamento. Lista recarregada (`carregarGerentesSugeridos()` de novo) depois de qualquer adição/remoção de gerente, em qualquer departamento, pra manter as sugestões sempre refletindo quem ainda falta mapear. O campo de texto livre continua disponível, pra gerentes que não apareceram na última apuração (colaborador novo, ainda sem apuração processada).
Validado com a apuração real do ambiente: as sugestões bateram exatamente com os 6 gerentes conhecidos (João Candido Rodrigues, Lhais Vergilio Delavy, Cristiano Silverio, Daniel Gustavo Manenti, Luciane Gonzaga, Elizangela de Paula Kuhn), já que nenhum deles está mapeado ainda no ambiente.
### 47. Bug: `SuspiciousFileOperation` ao anexar arquivo com nome muito longo na Importação de Plano de Saúde
Testando em produção real, upload de um arquivo da operadora com nome de arquivo original bem longo (ex.: "LEIAUTE_IMPORTACAO_DESPESAS_MEDICAS_EMP_92_PRESCINOTTI_CIA_LTDA...OPER_5060_UNIMED_DO_ESTADO_DO_PARANA_-_FEDERACAO_ESTADUAL_DA.CSV") deu 400 com `django.core.exceptions.SuspiciousFileOperation: Storage can not find an available filename ... Please make sure that the corresponding file field allows sufficient "max_length"`.
Causa: `ImportacaoPlanoSaude.planilha_padrao`/`arquivo_operadora` (`FileField`) não tinham `max_length` explícito — o padrão do Django é 100, insuficiente pra `upload_to="planos_saude/planilha_padrao/"` (ou `.../operadora/`) somado a um nome de arquivo original longo (nome de arquivo real do cliente, fora do controle do Portal) + o sufixo que a storage acrescenta pra evitar colisão.
Corrigido definindo `max_length=255` nos dois campos (`portal_api/models.py`, migração `0036_alter_importacaoplanosaude_arquivo_operadora_and_more`, aplicada no ambiente local). Mesmo cuidado vale pra qualquer `FileField`/`ImageField` novo que aceite nome de arquivo originado fora do Portal (upload do usuário) — o padrão de 100 caracteres do Django é apertado demais pra nomes de arquivo reais de operadoras/clientes.
### 48. Banco de regras de custeio salvas na Importação de Plano de Saúde
Pedido do usuário, testando a importação da empresa 92 (Unimed): em vez de exportar/importar um arquivo `.json` com a regra de custeio preenchida (mecanismo puramente client-side, sem persistência — nada guardado no banco, sem nome, sem observação), ele queria um banco de regras de verdade: salvar a configuração usada como "092 - Unimed", escolhê-la numa lista em importações futuras, poder editá-la depois e anexar uma observação livre (ex.: "Empresa não desconta plano do empregado XX").
O que mudou:
- **Model novo** `RegraCusteioPlanoSaude` (migração `0037_regracusteioplanosaude`) — `nome`/`operadora`/`tipos_lancamento`/`custeio_por_tipo` (mesmo formato dos campos homônimos de `ImportacaoPlanoSaude`) + `observacoes` (texto livre) + `criado_por`/`criado_em`/`atualizado_em`. Lista compartilhada, sem "dono", mesma permissão de toggle único da ferramenta (`PermissaoApp("utilitarios", "importacao-plano-saude")`).
- **`RegraCusteioPlanoSaudeViewSet`** (CRUD completo, GET/POST/PATCH/DELETE) registrado em `/api/regras-custeio-plano-saude/`, seguindo o mesmo padrão de `IndicadorPercentualTipoViewSet` (`perform_create` grava `criado_por`).
- **Validação de custeio extraída pra uma função compartilhada** (`_monta_regra_custeio()`, `serializers.py`) — antes só existia dentro de `ImportacaoPlanoSaudeCreateSerializer._valida_regra_especifica()`; extraída pra módulo-level e reaproveitada por `RegraCusteioPlanoSaudeSerializer.validate()`, pra não duplicar a regra de negócio (parsing BR, faixa 0–100 do percentual, "ao menos limite ou percentual") em dois serializers que podiam divergir com o tempo. `ImportacaoPlanoSaudeCreateSerializer` foi refatorado pra chamar essa mesma função — comportamento idêntico, validado com teste manual comparando a saída antes/depois do refactor.
- **Round-trip float↔texto BR**: uma regra salva volta do `GET` com `limite_valor`/`percentual` já como `float` (formato final persistido), mas a validação de entrada só entende texto BR (`"150,00"`). `_valor_custeio_para_texto_br()` normaliza um float de volta pra BR (via `formata_valor_br`, já existente em `leiaute_sistema.py`) antes de repassar pro parser — sem isso, reenviar uma regra sem editar o custeio (ex.: só corrigindo o nome) corromperia o valor (`"150.0"` seria lido como 15000 por `parse_valor_br`, que remove pontos como separador de milhar). Validado via shell: criar uma regra, pegar `validated_data` de volta e revalidar como se fosse um update sem mudanças reproduz exatamente o mesmo resultado.
- **Frontend** (`importacao-plano-saude.js`/`.html`/`.css`): a seção "Regra de custeio" do formulário de Nova Importação trocou os botões "Exportar regra"/"Importar regra" por um `<select>` de regras salvas + "Aplicar" (preenche o formulário inteiro, incluindo a operadora se ainda existir na lista — `aplicarRegraNoFormulario()`), "Salvar regra atual..." (abre `#ips-regra-save-modal` pra nomear/descrever, nascendo em modo "atualizar" quando a regra aplicada ainda existe, com uma checkbox pra virar "criar nova" em vez de sobrescrever) e "Ver regras salvas" (`#ips-regras-modal`, lista com Aplicar/Excluir por linha). "Editar" uma regra não é uma tela separada — é aplicar, ajustar o que quiser nos campos normais do formulário, e salvar de novo (decisão deliberada pra não duplicar a grade de custeio dentro de um segundo modal). A validação de "custeio completo pros tipos marcados" (`mensagemErroCusteio()`) foi extraída do handler do botão "Processar" pra ser reaproveitada por "Salvar regra atual..." também.
- Testado via Django test client (shell): criar/listar/atualizar/excluir uma regra pelo endpoint real, e confirmado que `criado_por` grava certo.
- **Ajuste de posição, no mesmo dia**: a pedido do usuário, a seção "Regra de custeio salva" moveu do final do formulário (depois de "Tipo de importação") pro **início**, antes até de "Operadora" — já que aplicar uma regra também preenche a operadora, faz mais sentido esse ser o primeiro passo do fluxo. A borda de separação (`.ips-regra-field`) virou `border-bottom` (era `border-top`), já que agora separa do campo abaixo (Operadora), não de cima.
### 49. Quarta operadora: Dental Uni Odonto
Usuário forneceu um PDF real ("1084 - RELATORIO DENTAL UNI 072026.pdf", relatório "BENEFICIÁRIOS") + a planilha padrão (leiaute Questor) já casada como referência, descrevendo o formato: coluna "Beneficiário" traz titular e dependentes juntos (dependentes com indentação um pouco maior), sem CPF pra ninguém, coluna "Valor Unit" é o valor a custear/descontar de cada um.
- **Novo parser** `operadoras/dental_uni/odonto_mensalidade.py` (`DentalUniOdontoMensalidade`), registrado em `pipeline.OPERADORAS` como `dental_uni_odonto_mensalidade`/"Dental Uni Odonto". `chave_casamento = "nome"` (sem CPF no arquivo, igual Unimed/Itamed) — só mensalidade (sem coluna de coparticipação nesse relatório).
- **Titular vs dependente por indentação, não por rótulo**: ao contrário da Itamed (que tem "Titula"/"Dependente" escrito no início da linha), este relatório não rotula nada — só indenta o texto do dependente um pouco mais que o do titular. O parser resolve isso comparando a indentação de cada linha com a indentação da primeira linha de beneficiário do arquivo (sempre um titular, por construção do relatório): igual ou menor → Titular; maior → Dependente.
- **Nome quebrado em duas linhas**: um titular do próprio exemplo ("SUZILAINE ZENATTI MEYER BEZERRA") tem o nome longo o bastante pra quebrar em duas linhas físicas no texto extraído do PDF, com o "[Nº Cartão]" só aparecendo na linha seguinte. O parser acumula linhas "órfãs" que parecem nome (só letras maiúsculas/espaços — nomes no relatório vêm 100% em caixa alta, o que distingue confiavelmente uma continuação de nome de qualquer outro texto do PDF, que nunca vem inteiramente maiúsculo) até encontrar a linha com o cartão, e usa a indentação da PRIMEIRA linha do bloco (não a da linha do cartão) pra decidir titular/dependente.
- **Extração do valor por padrão, não por posição de coluna**: como o número de datas antes do "Valor Unit" pode variar (ex.: uma linha com Data Exclusão preenchida teria uma data a mais), o parser não conta colunas — pega sempre o PRIMEIRO número no formato monetário (vírgula decimal) depois do "[Nº Cartão]", já que datas (`dd/mm/aaaa`) nunca coincidem com esse padrão. A coluna "Total Fam" (só preenchida na linha do titular, soma da família) é ignorada de propósito, mesmo espírito da "Valor Total" da Amil.
- **Validado com o exemplo real** (via script no shell do Django, não pelo formulário — ver caveat abaixo): os 11 lançamentos do PDF (4 famílias) foram extraídos corretamente, incluindo o nome quebrado em duas linhas, e o casamento com a planilha padrão fornecida bateu certo para 10 dos 11 — o 11º ("HELOISA NUNEZ RAMBO" no PDF vs "HELOISA NUNES RAMBO" na planilha, uma divergência real entre os dois arquivos de exemplo) caiu corretamente em auditoria (`NOME_DIVERGENTE`), exatamente o comportamento esperado (nunca resolvido por aproximação automática).
- **Caveat importante**: o parser foi escrito a partir do texto extraído do PDF mostrado na conversa, sem rodar o `pdfplumber` de verdade contra o arquivo binário (não ficou salvo em nenhum lugar acessível pelo ambiente de desenvolvimento). A indentação exata que o `pdfplumber` com `layout=True` vai produzir pro PDF real pode diferir da observada — o parser usa indentação *relativa* (comparada com a primeira linha do próprio arquivo, não um número fixo) exatamente para tolerar isso, mas só validar de verdade com o botão "Selecionar arquivo" (2. Arquivo da operadora) da tela de Nova Importação, que já chama esse parser isoladamente via `/importacoes-plano-saude/validar-arquivo/` sem precisar de uma importação completa — mesmo caminho que a Unimed também vai precisar percorrer antes de ter um PDF real (hoje `_extrai_pdf` da Unimed é só um `NotImplementedError` explícito por esse motivo).
### 50. Dois bugs corrigidos testando a Dental Uni com o PDF real
Dois problemas apareceram ao testar de fato (o "caveat" da rodada 49 se confirmou útil):
1. **Regressão em `ImportacaoPlanoSaudeDetailSerializer` (afetava TODAS as operadoras, não só a Dental Uni)**: `POST /api/importacoes-plano-saude/` dava 500 (`AttributeError: 'ImportacaoPlanoSaudeDetailSerializer' object has no attribute 'get_resumo_por_tipo'`) — o frontend mostrava só "Erro ao processar a solicitação." (mensagem genérica que `pidErrorMessageFrom()` usa quando a resposta não é JSON, ver `api.js`). Causa: ao inserir `RegraCusteioPlanoSaudeSerializer` logo depois de `ImportacaoPlanoSaudeDetailSerializer` na rodada 48, o método `get_resumo_por_tipo()` (que já existia, definido **depois** do `class Meta` da primeira classe) ficou fisicamente entre as duas — como Python não usa chaves pra delimitar classe, ele passou a pertencer à classe nova (`RegraCusteioPlanoSaudeSerializer`) por indentação, não à original. Corrigido movendo o método de volta pro lugar certo. Validado recriando uma importação completa via shell e conferindo que a serialização da resposta não quebra mais, além de reconfirmar que o CRUD de regras de custeio continua funcionando.
2. **Ordem de junção do nome quebrado em duas linhas estava invertida**: testando com o PDF real, "SUZILAINE ZENATTI MEYER" (titular) ficou sem "BEZERRA" (foi pra auditoria como pessoa não cadastrada, exigindo vínculo manual) e o dependente seguinte virou "BEZERRA JOAO LUCAS MEYER BEZERRA" (nem dava pra vincular, porque não existe ninguém com esse nome na planilha nem parecido o suficiente). A hipótese original (baseada só na inspeção visual do PDF, sem rodar o `pdfplumber` de verdade) era que o "[Nº Cartão]" e os valores apareciam *depois* de todas as linhas do nome; o comportamento real do `pdfplumber` é o oposto — o cartão/valores ficam grudados na *primeira* linha do nome, e o excedente (quando o nome quebra) sobra sozinho numa linha própria *depois*, antes do próximo beneficiário. Corrigido invertendo a lógica: cada linha com "[Nº Cartão]" agora é processada na hora (não espera nada depois dela); uma linha órfã em CAIXA ALTA sem colchete é anexada ao nome do **último** lançamento já adicionado (nunca ao próximo). Revalidado com um teste reproduzindo a estrutura real (cartão na linha do "SUZILAINE ZENATTI MEYER", "BEZERRA" sozinho na linha seguinte, "JOAO LUCAS MEYER BEZERRA" depois) — os 11 beneficiários das 4 famílias voltaram a bater certo, incluindo o titular com nome quebrado reconstituído corretamente e o dependente seguinte sem o prefixo indevido.
Lição prática: sem o PDF real rodando de fato no `pdfplumber`, a extração de texto mostrada por inspeção visual pode enganar sobre a ORDEM em que o excedente de uma célula quebrada aparece — vale sempre desconfiar de qualquer heurística de "juntar linhas" escrita sem testar contra o parser de verdade.
### 51. Bug (não específico da Dental Uni): planilha padrão em Windows-1252 quebrava a leitura
Testando com uma segunda empresa (planilha padrão com "SOPHIA FERNANDES GONÇALVES", um nome com "Ç"), o campo "1. Planilha padrão (Questor)" recusava o arquivo com "Este arquivo não parece ser a planilha padrão exportada do Questor...", mesmo o CSV tendo exatamente o cabeçalho esperado.
Causa: `le_planilha_padrao()` (`leiaute_sistema.py`) sempre abria o arquivo como `encoding="utf-8-sig"`, fixo. A planilha exportada do Questor, quando tem algum nome com acento, às vezes sai em **Windows-1252/ANSI**, não UTF-8 — decodificar um byte como `0xC7` ("Ç" em cp1252) como UTF-8 estoura `UnicodeDecodeError`. E como `_valida_planilha_padrao()`/`create()` capturam qualquer exceção genericamente (pra dar uma mensagem amigável quando o arquivo realmente está errado), o erro real (encoding) ficava escondido atrás da mensagem "não parece ser a planilha padrão" — nada a ver com o leiaute de colunas em si, que estava certo.
Corrigido com um fallback de encoding, mesmo espírito do `encoding="latin-1"` que o parser CSV da Unimed já usa: `_decodifica_planilha()` (nova função em `leiaute_sistema.py`) lê os bytes crus e tenta `utf-8-sig` primeiro (não muda nada pro caso comum sem acento, onde os bytes são idênticos nos dois formatos); só cai pra `cp1252` se a decodificação UTF-8 falhar. `le_planilha_padrao()` passou a ler de um `io.StringIO` sobre esse texto já decodificado, em vez de abrir o arquivo diretamente com um encoding fixo. Validado com teste cobrindo os 3 casos (UTF-8 sem BOM, UTF-8 com BOM, cp1252) e reproduzindo o arquivo real do usuário (17 beneficiários, valida certo agora).
Vale a mesma observação de robustez pro `arquivo_operadora` de qualquer operadora nova baseada em CSV (a Unimed já se protegeu disso; Dental Uni é PDF, não é afetada) — se aparecer o mesmo tipo de erro genérico de "formato não reconhecido" pra um CSV com acento, suspeitar de encoding antes de desconfiar do leiaute de colunas.
### 52. Quinta operadora: Unimed Oeste do Paraná
Usuário forneceu um PDF real ("Demonstrativo Junho.2026.pdf", "Resumo de Faturamento" emitido pela ACIME — associação comercial que fatura em nome da Unimed Oeste do Paraná) + a planilha padrão correspondente, descrevendo o formato: empregados e dependentes aparecem na coluna "Serviço/Produto", o TIPO (mensalidade/coparticipação) também é decidido por essa mesma coluna ("Convenio Unimed" = mensalidade, o resto = coparticipação), e o valor usado é "Val. Total".
- **Novo parser** `operadoras/unimed_oeste_pr/saude.py` (`UnimedOestePrSaude`), registrado em `pipeline.OPERADORAS` como `unimed_oeste_pr_saude`/"Unimed Oeste do Paraná" — **deliberadamente separado** do `unimed_saude` já existente, apesar do nome parecido: aquele espera um CSV com colunas próprias ("Id. Benef."/"Tipo Benef.", export direto da Unimed), este é um PDF de fatura da ACIME com um formato completamente diferente (nem CPF nem coluna de tipo dedicada). `chave_casamento = "nome"` (sem CPF no arquivo).
- **Cada pessoa pode ter mais de um "Nro." (contrato)** — ex.: "ALINE PATRICIA RAMOS" aparece em dois blocos "(T) ALINE PATRICIA RAMOS - Nro.: ..." com números de contrato diferentes (um pro plano base/Convênio, outro pro Aditivo de resgate aéreo). Por isso o parser agrupa por NOME (não por "Nro.", que varia por contrato da mesma pessoa), diferente de todas as operadoras anteriores que usavam um número de carteirinha/cartão estável por pessoa.
- **Tipo de lançamento decidido pelo texto da própria descrição, linha a linha** (não por bloco/contrato inteiro): dentro do MESMO bloco "Nro.", a linha "Convenio Unimed..." conta como mensalidade e a linha "Taxa Administrativa Unimed..." — que fica junto, no mesmo contrato — conta como coparticipação, por instrução explícita do usuário ("Convenio Unimed é o valor de mensalidade e os demais são coparticipação"). **Sinalizado ao usuário como algo a confirmar** — não é o desenho mais intuitivo (taxa administrativa normalmente anda junto do valor de mensalidade), mas foi implementado ao pé da letra da instrução recebida.
- **Duas variações de quebra de linha no PDF** precisaram de tratamento: (a) quando a coluna "Prestador" está vazia (ex. "ADITIVO UNIMED AIR TERRESTRE..."), a descrição e os 3 números (Qtd/Val.Unit/Val.Total) saem em linhas físicas separadas — o parser junta uma linha-só-texto com a linha-só-números que vem logo depois; (b) quando a coluna "Prestador" tem texto longo (ex. "ASSOCIACAO MISSIONARIA DE BENEFICENCIA DAS IRMAS SERVAS DO E"), esse texto transborda pra linha(s) DEPOIS dos números já lançados — como não sobra número nenhum nessas linhas de transbordo, elas são descartadas sem gerar lançamento extra (não precisamos do conteúdo de "Prestador" pra nada).
- **Validado com o PDF de exemplo completo**: reproduzindo as 4 pessoas (1 família de uma pessoa só + 1 família com titular e 2 dependentes), a soma de todos os lançamentos bateu exatamente com o "Total Faturados: 4.628,37" impresso no próprio PDF — confirma que nenhuma linha foi perdida nem contada em dobro, inclusive nos dois casos de quebra de linha acima. Casamento com a planilha padrão também testado (mensalidade e coparticipação separadas): as 4 pessoas casaram automaticamente, 0 itens de auditoria.
- Mesmo caveat das duas últimas rodadas: escrito a partir do texto extraído mostrado na conversa, sem rodar o `pdfplumber` de verdade contra o PDF binário — validar com o botão "Selecionar arquivo" antes de confiar em produção.
### 53. Regra de custeio salva: `<select>` virou combobox pesquisável
Com o banco de regras salvas crescendo (8 regras já cadastradas pelo usuário entre as 5 operadoras), o `<select>` nativo do campo "Regra de custeio salva" deixou de ser prático — sem busca, precisava rolar a lista inteira toda vez.
Trocado por um combobox pesquisável (`#ips-regra-combo`): um `<input type="text">` (`#ips-regra-search`) que funciona tanto como campo de busca quanto como "display" do valor selecionado, com uma lista flutuante (`#ips-regra-combo-list`, `position:absolute` abaixo do input) que filtra pelas regras cujo nome contém o texto digitado (case-insensitive) — abre no foco (mostrando todas) e a cada tecla digitada; fecha ao clicar fora (listener de `click` no `document`, checando `!ipsRegraCombo.contains(event.target)`) ou ao escolher um item. Mesmo espírito de busca+lista já usado em "Vincular pessoa", só que aqui o campo de busca dobra como o "valor exibido" no lugar de uma `<option>` selecionada.
Estado novo em JS: `regraSelecionadaId` (o que está de fato escolhido no combobox — diferente de `regraAplicadaId`, que reflete o que está refletido nos CAMPOS do formulário). Digitar de novo no campo depois de já ter selecionado algo invalida `regraSelecionadaId` até o usuário clicar numa regra da lista — sem isso, "Aplicar" poderia aplicar uma regra antiga enquanto o texto exibido já era outra busca, incoerência que o `<select>` antigo não tinha (mudar o texto de um `<select>` só é possível escolhendo uma opção de verdade).
`renderRegraSelect()` (populava as `<option>`) foi substituída por `renderRegraComboList(filtro)`; `refreshRegras()` deixou de re-renderizar um `<select>` inteiro e passou só a limpar a seleção se a regra escolhida tiver sido excluída em outro lugar enquanto isso (ex.: via o modal "Ver regras salvas").
## Roadmap / próximos passos
Nenhuma pendência explícita em aberto no momento, exceto a limitação conhecida
da rodada 45 (Luciane Gonzaga caindo em "Gerentes" em vez de "Rocket" quando o
usuário mapear os gerentes de verdade — ver rodada 45) — cada rodada acima foi
fechada a pedido do usuário. Ao retomar o projeto, perguntar o que vem a
seguir em vez de assumir. As migrações já foram rodadas até a `0035`
(rodada 45); o passo natural que falta é validar de ponta a ponta num
navegador de verdade um conjunto grande de funcionalidades que só foram
revisadas estaticamente ou testadas parcialmente: Links & Ferramentas
(rodada 16), a tela de Ramais completa (rodadas 21–23, 31–32), Acessos
Gerais (rodada 34) incluindo a restrição de seção por perfil e as
observações ricas com imagem, Eventos Corporativos no Calendário
Individual (rodada 35) com um perfil sem a permissão de criar evento,
Importação de Plano de Saúde (rodada 36) ponta a ponta com um arquivo
real de cada operadora, Simulação de Custo de Contratação (rodada 37)
conferindo o PDF gerado e a Lei 15.270/2025, e Indicador de Desempenho
(rodada 38) com uma apuração completa nova (fora do teste já feito com
os 43 colaboradores, que já validou o pipeline em si).

0
portal_api/__init__.py Normal file
View File

202
portal_api/admin.py Normal file
View File

@ -0,0 +1,202 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import (
AcessoGeral,
AcessoGeralSecao,
CompromissoAgenda,
Departamento,
Favorito,
FuncaoTelefonia,
ImportacaoPlanoSaude,
ImportacaoPlanoSaudeAuditoria,
ImportacaoPlanoSaudeLinha,
IndicadorApuracao,
IndicadorApuracaoColaborador,
IndicadorApuracaoEmpresa,
IndicadorApuracaoResposta,
IndicadorCriterio,
IndicadorDepartamento,
IndicadorDepartamentoGerente,
IndicadorPercentualTipo,
LinkFerramenta,
LinkFerramentaFavorito,
NotificacaoDispensada,
ParametroFiscalCustoContratacao,
PerfilAcesso,
RegraCusteioPlanoSaude,
TelefoneExterno,
Usuario,
WidgetUsuario,
)
@admin.register(Usuario)
class UsuarioAdmin(UserAdmin):
fieldsets = UserAdmin.fieldsets + (
("Portal De Paula", {"fields": ("nome", "perfis", "departamentos", "lideranca", "liderados")}),
)
filter_horizontal = UserAdmin.filter_horizontal + ("perfis", "departamentos", "liderados")
list_display = ("username", "nome", "is_active", "is_staff", "lideranca")
@admin.register(PerfilAcesso)
class PerfilAcessoAdmin(admin.ModelAdmin):
list_display = ("codigo", "nome", "ativo", "gerencia_permissoes")
search_fields = ("nome",)
@admin.register(Departamento)
class DepartamentoAdmin(admin.ModelAdmin):
list_display = ("nome",)
search_fields = ("nome",)
@admin.register(CompromissoAgenda)
class CompromissoAgendaAdmin(admin.ModelAdmin):
list_display = ("titulo", "dono", "data", "horario", "visibilidade", "lembrete_antecedencia")
list_filter = ("visibilidade", "lembrete_antecedencia")
search_fields = ("titulo",)
@admin.register(Favorito)
class FavoritoAdmin(admin.ModelAdmin):
list_display = ("usuario", "app_id")
search_fields = ("app_id",)
@admin.register(WidgetUsuario)
class WidgetUsuarioAdmin(admin.ModelAdmin):
list_display = ("usuario", "tipo")
@admin.register(NotificacaoDispensada)
class NotificacaoDispensadaAdmin(admin.ModelAdmin):
list_display = ("usuario", "notif_id", "criado_em")
search_fields = ("notif_id",)
@admin.register(LinkFerramenta)
class LinkFerramentaAdmin(admin.ModelAdmin):
list_display = ("nome", "url", "ordem")
ordering = ("ordem",)
search_fields = ("nome",)
@admin.register(AcessoGeralSecao)
class AcessoGeralSecaoAdmin(admin.ModelAdmin):
list_display = ("nome", "ordem")
ordering = ("ordem",)
search_fields = ("nome",)
filter_horizontal = ("perfis_restritos",)
@admin.register(AcessoGeral)
class AcessoGeralAdmin(admin.ModelAdmin):
list_display = ("nome", "secao", "usuario", "ordem")
ordering = ("secao__ordem", "ordem")
search_fields = ("nome", "usuario")
@admin.register(LinkFerramentaFavorito)
class LinkFerramentaFavoritoAdmin(admin.ModelAdmin):
list_display = ("usuario", "link")
@admin.register(TelefoneExterno)
class TelefoneExternoAdmin(admin.ModelAdmin):
list_display = ("nome", "ramal", "telefone")
search_fields = ("nome", "observacoes")
@admin.register(FuncaoTelefonia)
class FuncaoTelefoniaAdmin(admin.ModelAdmin):
list_display = ("comando", "resumo")
search_fields = ("comando", "resumo")
@admin.register(ImportacaoPlanoSaude)
class ImportacaoPlanoSaudeAdmin(admin.ModelAdmin):
list_display = ("nome_operadora", "tipos_lancamento", "status", "criado_por", "criado_em")
list_filter = ("operadora", "status")
search_fields = ("nome_operadora",)
@admin.register(ImportacaoPlanoSaudeLinha)
class ImportacaoPlanoSaudeLinhaAdmin(admin.ModelAdmin):
list_display = ("importacao", "tipo_lancamento", "nome_func", "valor_empresa", "valor")
list_filter = ("tipo_lancamento",)
search_fields = ("nome_func", "cpf_func", "nome_dependente")
@admin.register(ImportacaoPlanoSaudeAuditoria)
class ImportacaoPlanoSaudeAuditoriaAdmin(admin.ModelAdmin):
list_display = ("importacao", "nome", "motivo", "tipo_lancamento")
list_filter = ("motivo",)
search_fields = ("nome",)
@admin.register(RegraCusteioPlanoSaude)
class RegraCusteioPlanoSaudeAdmin(admin.ModelAdmin):
list_display = ("nome", "operadora", "tipos_lancamento", "criado_por", "atualizado_em")
search_fields = ("nome", "operadora")
@admin.register(ParametroFiscalCustoContratacao)
class ParametroFiscalCustoContratacaoAdmin(admin.ModelAdmin):
list_display = ("__str__", "atualizado_em")
@admin.register(IndicadorDepartamento)
class IndicadorDepartamentoAdmin(admin.ModelAdmin):
list_display = ("nome", "ativo")
search_fields = ("nome",)
@admin.register(IndicadorDepartamentoGerente)
class IndicadorDepartamentoGerenteAdmin(admin.ModelAdmin):
list_display = ("nome_gerente", "departamento")
list_filter = ("departamento",)
search_fields = ("nome_gerente",)
@admin.register(IndicadorPercentualTipo)
class IndicadorPercentualTipoAdmin(admin.ModelAdmin):
list_display = (
"departamento", "tipo", "percentual_individual", "percentual_grupo", "percentual_departamento", "vigente_desde"
)
list_filter = ("departamento", "tipo")
ordering = ("departamento", "tipo", "-vigente_desde")
@admin.register(IndicadorCriterio)
class IndicadorCriterioAdmin(admin.ModelAdmin):
list_display = ("nome", "departamento", "grupo", "peso", "periodo", "papel_aplicavel", "calculo_automatico", "ativo")
list_filter = ("departamento", "grupo", "periodo", "ativo")
search_fields = ("nome",)
@admin.register(IndicadorApuracao)
class IndicadorApuracaoAdmin(admin.ModelAdmin):
list_display = ("competencia", "status", "criado_por", "criado_em")
list_filter = ("status",)
ordering = ("-competencia",)
@admin.register(IndicadorApuracaoColaborador)
class IndicadorApuracaoColaboradorAdmin(admin.ModelAdmin):
list_display = ("apuracao", "nome", "gerente", "valor_total")
search_fields = ("nome", "gerente")
@admin.register(IndicadorApuracaoEmpresa)
class IndicadorApuracaoEmpresaAdmin(admin.ModelAdmin):
list_display = ("colaborador", "nome_empresa", "tipo", "honorario", "valor_total")
list_filter = ("tipo", "honorario_nao_encontrado")
search_fields = ("nome_empresa", "codigo_empresa")
@admin.register(IndicadorApuracaoResposta)
class IndicadorApuracaoRespostaAdmin(admin.ModelAdmin):
list_display = ("colaborador", "criterio", "valor", "ajustado_manualmente")
list_filter = ("valor", "ajustado_manualmente")

7
portal_api/apps.py Normal file
View File

@ -0,0 +1,7 @@
from django.apps import AppConfig
class PortalApiConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "portal_api"
verbose_name = "Portal De Paula — API"

202
portal_api/catalogo.py Normal file
View File

@ -0,0 +1,202 @@
"""Catálogo de módulos/aplicações do menu do Portal De Paula.
Fonte única da verdade para o que existe no sidebar — o frontend consome isso
via GET /api/catalogo/ em vez de manter uma cópia própria (evita as duas
pontas saírem de sincronia). Mesma estrutura que já existia em
assets/js/profiles.js (PID_MODULES/PID_MODULE_APPS) antes da migração.
"""
from typing import Any
MODULES = [
{"key": "principal", "label": "Principal"},
{"key": "ramais", "label": "Ramais"},
{"key": "calendario", "label": "Calendário De Paula"},
{"key": "calendario-individual", "label": "Calendário Individual"},
{"key": "portais", "label": "Portais"},
{"key": "geradoc", "label": "Geradoc"},
{"key": "relatorios", "label": "Relatórios"},
{"key": "relatorios-gerenciais", "label": "Relatórios Gerenciais"},
{"key": "utilitarios", "label": "Utilitários"},
{"key": "integracoes", "label": "Integrações"},
{"key": "auditorias", "label": "Auditorias"},
{"key": "links-ferramentas", "label": "Links & Ferramentas"},
{"key": "solicitacoes", "label": "Solicitações"},
{"key": "administracao", "label": "Administração"},
]
# Cada entrada é uma aplicação direta ({"key", "label"}) ou um subgrupo com
# ferramentas ({"key", "label", "tools": [...]}) — mesmo formato de dois
# níveis usado hoje em PID_MODULE_APPS (ver Auditorias abaixo).
MODULE_APPS = {
"portais": [
{"key": "portal-do-cliente", "label": "Portal do Cliente"},
{"key": "portal-fiscal", "label": "Portal Fiscal"},
],
# "Gerar Contrato"/"Gerar Procuração" existiam como placeholders (href="#")
# e foram removidos por não serem usados — decisão explícita do usuário,
# não recriar sem confirmar de novo (mesmo espírito da remoção de
# "Conversor de Arquivos"/"Calculadora Fiscal" de Utilitários).
"geradoc": [
{"key": "simulacao-custo-contratacao", "label": "Simulação de Custo de Contratação"},
{"key": "indicador-desempenho", "label": "Indicador de Desempenho"},
],
"relatorios": [
{"key": "relatorio-setorial", "label": "Relatório Setorial"},
{"key": "indicadores", "label": "Indicadores"},
],
"relatorios-gerenciais": [
{"key": "visao-diretoria", "label": "Visão Diretoria"},
{"key": "visao-gerencial", "label": "Visão Gerencial"},
],
"utilitarios": [
{"key": "importacao-plano-saude", "label": "Importação de Plano de Saúde"},
],
"integracoes": [
{"key": "questor", "label": "Questor"},
],
"auditorias": [
{
"key": "consultoria-tributaria",
"label": "Consultoria Tributária",
"tools": [{"key": "controle-simples-nacional", "label": "Controle Simples Nacional"}],
},
{
"key": "fisco-contabil",
"label": "Fisco/Contábil",
"tools": [{"key": "auditoria-de-balancetes", "label": "Auditoria de Balancetes"}],
},
],
# Cada entrada é um tópico que abre um link externo (Asana, hoje) numa aba nova
# — não são telas próprias, a URL de cada um vive só no <a href> do sidebar
# (não dá pra embutir em iframe: o Asana bloqueia isso via X-Frame-Options/CSP,
# já foi tentado e revertido). "Ordem de Serviço" não tem link ainda (será
# reformulada depois), por isso continua com href="#" no sidebar.
"solicitacoes": [
{"key": "controle-notificacoes", "label": "Controle de Notificações"},
{"key": "ordem-servico", "label": "Ordem de Serviço"},
{"key": "pimi", "label": "P.I.M.I."},
{"key": "programa-indicacoes", "label": "Programa de Indicações"},
{"key": "pos-vendas", "label": "Pós-Vendas"},
{"key": "sugestao-inovacao", "label": "Sugestão de Inovação"},
],
# "Links & Ferramentas" virou uma seção com 2 aplicações reais (cada uma com sua
# própria tela) — a grade de cartões original e "Acessos Gerais" (cadastro de
# logins/acessos compartilhados, organizado em seções e linhas). Cada uma delas
# não é "uma aplicação" no sentido dos módulos com subitens simples (portais,
# geradoc etc.) — tem um par visualizar/editar próprio, modelado como subgrupo
# (mesmo formato de "tools" de Auditorias/Ramais) pra reaproveitar 100% a árvore
# de permissões genérica (profiles.js) sem nenhum código de UI novo.
"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)"},
],
},
],
# Ramais virou uma seção com 5 subtelas (abas em ramais.html) — cada uma com seu
# próprio "visualizar", e só as 3 com conteúdo administrável (Ramais, Telefones
# Externos, Funções de Telefonia) também com "editar". Modelado como subgrupo
# (mesmo formato de "tools" usado em Auditorias) pra reaproveitar 100% a árvore
# de permissões genérica (profiles.js) sem nenhum código de UI novo.
# Calendário Individual é "base" (visível a todo perfil) e não tem
# visualizar/editar geral — só a criação/edição de eventos de departamento
# ou para todos (compromissos corporativos, distintos dos pessoais
# "somente eu") e o cadastro de categorias de evento são restritos, daí um
# único "tool" em vez do par visualizar/editar completo.
"calendario-individual": [
{
"key": "calendario-individual-eventos",
"label": "Eventos Corporativos",
"tools": [
{
"key": "calendario-individual-criar-evento",
"label": "Criar e gerenciar eventos de departamento/todos e categorias de evento",
}
],
}
],
"ramais": [
{
"key": "ramais-diretorio",
"label": "Ramais",
"tools": [
{"key": "ramais-visualizar", "label": "Visualizar"},
{"key": "ramais-editar", "label": "Editar (adicionar/alterar ramal e criar ausência)"},
],
},
{
"key": "responsavel-tareffa",
"label": "Responsável no Tareffa",
"tools": [{"key": "responsavel-tareffa-visualizar", "label": "Visualizar"}],
},
{
"key": "telefones-externos",
"label": "Telefones Externos",
"tools": [
{"key": "telefones-externos-visualizar", "label": "Visualizar"},
{"key": "telefones-externos-editar", "label": "Editar"},
],
},
{
"key": "ferias",
"label": "Férias",
"tools": [{"key": "ferias-visualizar", "label": "Visualizar"}],
},
{
"key": "funcoes-telefonia",
"label": "Funções de Telefonia",
"tools": [
{"key": "funcoes-telefonia-visualizar", "label": "Visualizar"},
{"key": "funcoes-telefonia-editar", "label": "Editar"},
],
},
],
}
BASE_KEYS = ["principal", "ramais", "calendario", "calendario-individual", "links-ferramentas", "solicitacoes"]
SECTORAL_KEYS = ["portais", "geradoc", "relatorios", "utilitarios", "integracoes", "auditorias"]
def app_keys_for(module_key: str) -> list[str]:
"""Lista achatada de chaves de aplicação/ferramenta de um módulo (entra em subgrupos)."""
keys = []
for entry in MODULE_APPS.get(module_key, []):
if "tools" in entry:
keys.extend(tool["key"] for tool in entry["tools"])
else:
keys.append(entry["key"])
return keys
def empty_permissions(all_enabled: bool = False) -> dict[str, Any]:
perms = {}
for module in MODULES:
key = module["key"]
perms[key] = {
"enabled": bool(all_enabled),
"apps": {app_key: bool(all_enabled) for app_key in app_keys_for(key)},
}
return perms
def permissions_from_keys(enabled_keys: list[str]) -> dict[str, Any]:
perms = empty_permissions(False)
for key in enabled_keys:
perms[key] = {"enabled": True, "apps": {app_key: True for app_key in app_keys_for(key)}}
return perms
def catalogo_payload() -> dict[str, Any]:
return {"modulos": MODULES, "aplicacoes_por_modulo": MODULE_APPS}

View File

View File

@ -0,0 +1,251 @@
"""Cálculo do custo de contratação — regime Empregado CLT.
Fórmulas portadas 1:1 da planilha de referência do escritório
(`projects/planilha de custo/1972 - CUSTO EMPREGADO SALÁRIO.xlsx`), com duas
correções deliberadas e explicitamente pedidas pelo usuário:
1. A planilha original nunca soma a dedução por dependente à base do IRRF
quando usa o desconto real de INSS (só quando usa o desconto simplificado
— onde a dedução por dependente não se aplica por lei, já que os dois
métodos são mutuamente exclusivos). Aqui, quando o desconto real de INSS
é o escolhido, a dedução por dependente também é subtraída da base do
IRRF.
2. A partir de janeiro/2026, a Lei nº 15.270/2025 (art. 3º-A da Lei nº
9.250/1995) passou a reduzir o IRRF já calculado pela tabela progressiva
tradicional — ver `_reducao_lei_15270` abaixo. Essa lei não mudou a
tabela progressiva em si, só acrescenta essa redução por cima.
Os parâmetros fiscais (faixas de INSS/IRRF, dedução por dependente, os
coeficientes da Lei 15.270/2025 etc.) não são mais constantes deste módulo —
mudam todo ano, então vêm de fora via `ParametrosFiscais` (montado a partir
de `ParametroFiscalCustoContratacao`, editável pela própria tela; ver
`portal_api/models.py`). `tabelas.py` continua existindo só como o
seed/default dessa configuração.
"""
from dataclasses import dataclass
@dataclass
class ParametrosFiscais:
faixas_inss: list[tuple[float, float, float]]
teto_desconto_inss: float
faixas_irrf: list[tuple[float, float, float]]
aliquota_irrf_topo: float
deduzir_irrf_topo: float
desconto_simplificado_irrf: float
deducao_por_dependente: float
reducao_lei_15270_coeficiente_a: float
reducao_lei_15270_coeficiente_b: float
reducao_lei_15270_limite: float
@dataclass
class EntradaSimulacaoEmpregado:
nome_referencia: str
empresa: str
cargo: str
salario_contratual: float
adicional_art62: float
comissao: float
dsr: float
outros_itens_remuneracao: list[tuple[str, float]] # somam à base de INSS/FGTS/13º/férias/IRRF, como salário
percentual_inss_empregador: float # fração, ex.: 0.26
percentual_fgts: float # fração, ex.: 0.08
dependentes: int
vale_transporte: float
seguro_vida: float
vale_alimentacao: float
assiduidade: float
fundo_formacao: float
outros_beneficios: list[tuple[str, float]] # custo variável do empregador, cada item some no PDF
desconto_vale_transporte: float
desconto_vale_alimentacao: float
desconto_sindicato: float
outros_descontos: list[tuple[str, float]] # descontado direto do líquido do empregado
@dataclass
class ResultadoSimulacaoEmpregado:
entrada: EntradaSimulacaoEmpregado
inss_salario: float
fgts_salario: float
decimo_terceiro_avo: float
inss_decimo_terceiro: float
fgts_decimo_terceiro: float
ferias_avo: float
ferias_um_terco: float
inss_ferias: float
fgts_ferias: float
multa_40_fgts: float
subtotal_custos_fixos: float
subtotal_custos_variaveis: float
custo_total_empresa: float
base_inss_empregado: float
aliquota_inss_empregado: float
desconto_inss_empregado: float
base_irrf: float
aliquota_irrf: float
desconto_irrf_tabela: float
reducao_lei_15270: float
desconto_irrf: float
liquido_empregado: float
def _desconto_inss(base: float, parametros: ParametrosFiscais) -> tuple[float, float]:
"""(alíquota, desconto) do INSS do empregado — progressivo, com teto fixo."""
for limite, aliquota, deduzir in parametros.faixas_inss:
if base <= limite:
return aliquota, (base * aliquota) - deduzir
return 0.0, parametros.teto_desconto_inss
def _desconto_irrf_tabela(base: float, parametros: ParametrosFiscais) -> tuple[float, float]:
"""(alíquota, desconto) do IRRF pela tabela progressiva tradicional;
desconto zerado se <= R$10 (regra de dispensa de retenção pra valores
muito baixos, já presente na planilha original)."""
aliquota, deduzir = parametros.aliquota_irrf_topo, parametros.deduzir_irrf_topo
for limite, faixa_aliquota, faixa_deduzir in parametros.faixas_irrf:
if base <= limite:
aliquota, deduzir = faixa_aliquota, faixa_deduzir
break
desconto_bruto = (base * aliquota) - deduzir
return aliquota, desconto_bruto if desconto_bruto > 10 else 0.0
def _reducao_lei_15270(rendimento_bruto_mensal: float, parametros: ParametrosFiscais) -> float:
"""Redução adicional de IRRF da Lei nº 15.270/2025 — isenção total até
R$5.000,00 de rendimento bruto mensal, decrescente até zerar em
`reducao_lei_15270_limite` (R$7.350,00 na regra vigente). Aplicada sobre
o rendimento bruto (antes do INSS), não sobre a base já líquida de INSS."""
if rendimento_bruto_mensal >= parametros.reducao_lei_15270_limite:
return 0.0
reducao = parametros.reducao_lei_15270_coeficiente_a - (
parametros.reducao_lei_15270_coeficiente_b * rendimento_bruto_mensal
)
return max(0.0, reducao)
def calcula_custo_empregado(
entrada: EntradaSimulacaoEmpregado, parametros: ParametrosFiscais
) -> ResultadoSimulacaoEmpregado:
perc_inss = entrada.percentual_inss_empregador
perc_fgts = entrada.percentual_fgts
base_remuneracao = (
entrada.salario_contratual
+ entrada.adicional_art62
+ entrada.comissao
+ entrada.dsr
+ sum(valor for _, valor in entrada.outros_itens_remuneracao)
)
inss_salario = base_remuneracao * perc_inss
fgts_salario = base_remuneracao * perc_fgts
decimo_terceiro_avo = base_remuneracao / 12
inss_decimo_terceiro = decimo_terceiro_avo * perc_inss
fgts_decimo_terceiro = decimo_terceiro_avo * perc_fgts
ferias_avo = base_remuneracao / 12
ferias_um_terco = ferias_avo / 3
inss_ferias = (ferias_avo + ferias_um_terco) * perc_inss
fgts_ferias = (ferias_avo + ferias_um_terco) * perc_fgts
multa_40_fgts = (fgts_salario + fgts_ferias + fgts_decimo_terceiro) * 0.4
subtotal_custos_fixos = (
base_remuneracao
+ inss_salario
+ fgts_salario
+ decimo_terceiro_avo
+ inss_decimo_terceiro
+ fgts_decimo_terceiro
+ ferias_avo
+ ferias_um_terco
+ inss_ferias
+ fgts_ferias
+ multa_40_fgts
)
subtotal_custos_variaveis = (
entrada.vale_transporte
+ entrada.seguro_vida
+ entrada.vale_alimentacao
+ entrada.assiduidade
+ entrada.fundo_formacao
+ sum(valor for _, valor in entrada.outros_beneficios)
)
custo_total_empresa = subtotal_custos_fixos + subtotal_custos_variaveis
base_inss_empregado = base_remuneracao
aliquota_inss_empregado, desconto_inss_empregado = _desconto_inss(base_inss_empregado, parametros)
if desconto_inss_empregado >= parametros.desconto_simplificado_irrf:
base_irrf = (
base_inss_empregado
- desconto_inss_empregado
- (entrada.dependentes * parametros.deducao_por_dependente)
)
else:
base_irrf = base_inss_empregado - parametros.desconto_simplificado_irrf
aliquota_irrf, desconto_irrf_tabela = _desconto_irrf_tabela(base_irrf, parametros)
reducao_lei_15270 = _reducao_lei_15270(base_remuneracao, parametros)
desconto_irrf = max(0.0, desconto_irrf_tabela - reducao_lei_15270)
total_descontos_diversos = (
entrada.desconto_vale_transporte
+ entrada.desconto_vale_alimentacao
+ entrada.desconto_sindicato
+ sum(valor for _, valor in entrada.outros_descontos)
)
liquido_empregado = base_remuneracao - desconto_inss_empregado - desconto_irrf - total_descontos_diversos
return ResultadoSimulacaoEmpregado(
entrada=entrada,
inss_salario=inss_salario,
fgts_salario=fgts_salario,
decimo_terceiro_avo=decimo_terceiro_avo,
inss_decimo_terceiro=inss_decimo_terceiro,
fgts_decimo_terceiro=fgts_decimo_terceiro,
ferias_avo=ferias_avo,
ferias_um_terco=ferias_um_terco,
inss_ferias=inss_ferias,
fgts_ferias=fgts_ferias,
multa_40_fgts=multa_40_fgts,
subtotal_custos_fixos=subtotal_custos_fixos,
subtotal_custos_variaveis=subtotal_custos_variaveis,
custo_total_empresa=custo_total_empresa,
base_inss_empregado=base_inss_empregado,
aliquota_inss_empregado=aliquota_inss_empregado,
desconto_inss_empregado=desconto_inss_empregado,
base_irrf=base_irrf,
aliquota_irrf=aliquota_irrf,
desconto_irrf_tabela=desconto_irrf_tabela,
reducao_lei_15270=reducao_lei_15270,
desconto_irrf=desconto_irrf,
liquido_empregado=liquido_empregado,
)
def formata_moeda(valor: float) -> str:
"""8500.0 -> 'R$ 8.500,00'; -12.3 -> '-R$ 12,30'."""
negativo = valor < 0
inteiro, decimal = f"{abs(valor):,.2f}".split(".")
texto = f"R$ {inteiro.replace(',', '.')},{decimal}"
return f"-{texto}" if negativo else texto
def formata_percentual(fracao: float) -> str:
"""0.26 -> '26,00%'."""
return f"{fracao * 100:.2f}".replace(".", ",") + "%"

View File

@ -0,0 +1,293 @@
"""Geração do PDF de "Simulação de Custo de Contratação" — identidade visual
do escritório (dourado/marrom do logo, cores extraídas de `static/img/logo.png`),
pronto para ser enviado ao cliente."""
import io
from pathlib import Path
from PIL import Image as PILImage
from reportlab.lib import colors
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import cm
from reportlab.pdfgen.canvas import Canvas
from reportlab.platypus import SimpleDocTemplate, Spacer, Table, TableStyle
from .calculo import ResultadoSimulacaoEmpregado, formata_moeda, formata_percentual
_LOGO_BRANCO_PATH = Path(__file__).resolve().parents[2] / "static" / "img" / "logo-branco.png"
# Cores extraídas do próprio static/img/logo.png (pixel do "D" e da sombra do
# degradê) — a identidade visual real do escritório, não o roxo do tema do
# Portal (que é só a cor de interface escolhida pelo usuário logado).
_COR_GOLD = colors.HexColor("#D3AF4D")
_COR_GOLD_ESCURO = colors.HexColor("#A98C3E")
_COR_MARROM = colors.HexColor("#4A3C28")
_COR_GOLD_CLARO_BANNER = colors.HexColor("#CBB98C")
_COR_TEXTO = colors.HexColor("#2A2118")
_COR_TEXTO_MUTED = colors.HexColor("#7A6F5D")
_COR_LINHA = colors.HexColor("#E7DEC7")
_COR_ZEBRA = colors.HexColor("#FAF6EC")
_COR_SUBTOTAL_BG = colors.HexColor("#F0E0B0")
_LARGURA_PAGINA, _ALTURA_PAGINA = A4
_MARGEM = 1.8 * cm
_ALTURA_BANNER = 3.1 * cm
_LARGURA_COL_ROTULO = 11 * cm
_LARGURA_COL_VALOR = 4.5 * cm
def _dimensoes_imagem(caminho: Path, largura_pt: float) -> tuple[float, float]:
with PILImage.open(caminho) as img:
largura_px, altura_px = img.size
return largura_pt, largura_pt * (altura_px / largura_px)
def _linha_identificacao(nome_referencia: str, empresa: str, cargo: str) -> str:
partes = [parte for parte in (nome_referencia, empresa, cargo) if parte]
return " — ".join(partes) if partes else "De Paula Contadores"
def _desenha_banner(canvas: Canvas, nome_referencia: str, empresa: str, cargo: str, gerado_em: str) -> None:
topo = _ALTURA_PAGINA
canvas.setFillColor(_COR_MARROM)
canvas.rect(0, topo - _ALTURA_BANNER, _LARGURA_PAGINA, _ALTURA_BANNER, stroke=0, fill=1)
canvas.setFillColor(_COR_GOLD)
canvas.rect(0, topo - _ALTURA_BANNER - 0.1 * cm, _LARGURA_PAGINA, 0.1 * cm, stroke=0, fill=1)
logo_largura, logo_altura = _dimensoes_imagem(_LOGO_BRANCO_PATH, 3.4 * cm)
logo_y = topo - _ALTURA_BANNER / 2 - logo_altura / 2
canvas.drawImage(
str(_LOGO_BRANCO_PATH), _MARGEM, logo_y, width=logo_largura, height=logo_altura, mask="auto"
)
texto_x = _MARGEM + logo_largura + 0.7 * cm
centro_y = topo - _ALTURA_BANNER / 2
canvas.setFillColor(colors.white)
canvas.setFont("Helvetica-Bold", 17)
canvas.drawString(texto_x, centro_y + 0.22 * cm, "Simulação de Custo de Contratação")
canvas.setFont("Helvetica", 10.5)
canvas.setFillColor(_COR_GOLD)
canvas.drawString(texto_x, centro_y - 0.32 * cm, _linha_identificacao(nome_referencia, empresa, cargo))
canvas.setFont("Helvetica", 8.5)
canvas.setFillColor(_COR_GOLD_CLARO_BANNER)
canvas.drawString(texto_x, centro_y - 0.72 * cm, f"Gerado em {gerado_em}")
def _desenha_rodape(canvas: Canvas) -> None:
canvas.setStrokeColor(_COR_GOLD_ESCURO)
canvas.setLineWidth(0.6)
canvas.line(_MARGEM, 1.55 * cm, _LARGURA_PAGINA - _MARGEM, 1.55 * cm)
canvas.setFont("Helvetica", 7.5)
canvas.setFillColor(_COR_TEXTO_MUTED)
canvas.drawCentredString(
_LARGURA_PAGINA / 2,
1.15 * cm,
"De Paula Contadores · Simulação de caráter estimativo, sujeita à confirmação",
)
def _pagina(
canvas: Canvas, doc: SimpleDocTemplate, nome_referencia: str, empresa: str, cargo: str, gerado_em: str
) -> None:
canvas.saveState()
_desenha_banner(canvas, nome_referencia, empresa, cargo, gerado_em)
_desenha_rodape(canvas)
canvas.setFont("Helvetica", 7.5)
canvas.setFillColor(_COR_TEXTO_MUTED)
canvas.drawRightString(_LARGURA_PAGINA - _MARGEM, 1.15 * cm, f"Página {doc.page}")
canvas.restoreState()
def _tabela_secao(
titulo: str,
linhas: list[tuple[str, str]],
indices_subtotal: frozenset[int] = frozenset(),
indices_total: frozenset[int] = frozenset(),
) -> Table:
"""Uma "seção" é uma única Table: a primeira linha é a barra de título
(dourada, colspan completo) e as demais são os pares rótulo/valor —
dá o efeito de card com cabeçalho colorido sem precisar de um Paragraph
separado antes da tabela."""
dados = [[titulo, ""]] + [[rotulo, valor] for rotulo, valor in linhas]
tabela = Table(dados, colWidths=[_LARGURA_COL_ROTULO, _LARGURA_COL_VALOR])
estilo = [
("SPAN", (0, 0), (-1, 0)),
("BACKGROUND", (0, 0), (-1, 0), _COR_GOLD),
("TEXTCOLOR", (0, 0), (-1, 0), _COR_MARROM),
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
("FONTSIZE", (0, 0), (-1, 0), 10.5),
("TOPPADDING", (0, 0), (-1, 0), 7),
("BOTTOMPADDING", (0, 0), (-1, 0), 7),
("LEFTPADDING", (0, 0), (-1, -1), 10),
("RIGHTPADDING", (0, 0), (-1, -1), 10),
("FONTNAME", (0, 1), (-1, -1), "Helvetica"),
("FONTSIZE", (0, 1), (-1, -1), 9.5),
("TEXTCOLOR", (0, 1), (-1, -1), _COR_TEXTO),
("ALIGN", (1, 0), (1, -1), "RIGHT"),
("LINEBELOW", (0, 1), (-1, -2), 0.5, _COR_LINHA),
("TOPPADDING", (0, 1), (-1, -1), 4.5),
("BOTTOMPADDING", (0, 1), (-1, -1), 4.5),
("BOX", (0, 0), (-1, -1), 0.75, _COR_GOLD_ESCURO),
]
linhas_especiais = {indice + 1 for indice in (indices_subtotal | indices_total)}
for i in range(1, len(dados)):
if i not in linhas_especiais and (i % 2 == 0):
estilo.append(("BACKGROUND", (0, i), (-1, i), _COR_ZEBRA))
for indice in indices_subtotal:
i = indice + 1
estilo += [
("BACKGROUND", (0, i), (-1, i), _COR_SUBTOTAL_BG),
("FONTNAME", (0, i), (-1, i), "Helvetica-Bold"),
]
for indice in indices_total:
i = indice + 1
estilo += [
("BACKGROUND", (0, i), (-1, i), _COR_MARROM),
("TEXTCOLOR", (0, i), (-1, i), colors.white),
("FONTNAME", (0, i), (-1, i), "Helvetica-Bold"),
("FONTSIZE", (0, i), (-1, i), 10.5),
("TOPPADDING", (0, i), (-1, i), 7),
("BOTTOMPADDING", (0, i), (-1, i), 7),
]
tabela.setStyle(TableStyle(estilo))
return tabela
def _secao_dados_informados(resultado: ResultadoSimulacaoEmpregado) -> Table:
entrada = resultado.entrada
linhas = [
("Salário contratual", formata_moeda(entrada.salario_contratual)),
("Adicional Art. 62.", formata_moeda(entrada.adicional_art62)),
("Comissão", formata_moeda(entrada.comissao)),
("DSR", formata_moeda(entrada.dsr)),
]
linhas += [(nome, formata_moeda(valor)) for nome, valor in entrada.outros_itens_remuneracao]
linhas += [
("% de INSS Empregador", formata_percentual(entrada.percentual_inss_empregador)),
("% de FGTS", formata_percentual(entrada.percentual_fgts)),
("Dependentes menores de 21 anos", str(entrada.dependentes)),
("Vale Transporte", formata_moeda(entrada.vale_transporte)),
("Seguro de vida", formata_moeda(entrada.seguro_vida)),
("Vale Alimentação", formata_moeda(entrada.vale_alimentacao)),
("Assiduidade", formata_moeda(entrada.assiduidade)),
("Fundo de Formação", formata_moeda(entrada.fundo_formacao)),
]
linhas += [(nome, formata_moeda(valor)) for nome, valor in entrada.outros_beneficios]
linhas += [
("Desconto Vale Transporte", formata_moeda(entrada.desconto_vale_transporte)),
("Desconto Vale Alimentação", formata_moeda(entrada.desconto_vale_alimentacao)),
("Desconto Sindicato", formata_moeda(entrada.desconto_sindicato)),
]
linhas += [(nome, formata_moeda(valor)) for nome, valor in entrada.outros_descontos]
return _tabela_secao("DADOS INFORMADOS", linhas)
def _secao_custo_empregador(resultado: ResultadoSimulacaoEmpregado) -> Table:
entrada = resultado.entrada
linhas = [
("Salário", formata_moeda(entrada.salario_contratual)),
("Adicional Art. 62.", formata_moeda(entrada.adicional_art62)),
("Comissão", formata_moeda(entrada.comissao)),
("DSR", formata_moeda(entrada.dsr)),
]
linhas += [(nome, formata_moeda(valor)) for nome, valor in entrada.outros_itens_remuneracao]
linhas += [
("INSS s/ salário", formata_moeda(resultado.inss_salario)),
("FGTS s/ salário", formata_moeda(resultado.fgts_salario)),
("13º salário (1/12)", formata_moeda(resultado.decimo_terceiro_avo)),
("INSS s/ 13º", formata_moeda(resultado.inss_decimo_terceiro)),
("FGTS s/ 13º", formata_moeda(resultado.fgts_decimo_terceiro)),
("Férias (1/12)", formata_moeda(resultado.ferias_avo)),
("1/3 Constitucional", formata_moeda(resultado.ferias_um_terco)),
("INSS s/ férias", formata_moeda(resultado.inss_ferias)),
("FGTS s/ férias", formata_moeda(resultado.fgts_ferias)),
("Multa 40% FGTS", formata_moeda(resultado.multa_40_fgts)),
]
indice_subtotal_fixos = len(linhas)
linhas.append(("Subtotal Custos Fixos", formata_moeda(resultado.subtotal_custos_fixos)))
linhas += [
("Vale Transporte", formata_moeda(entrada.vale_transporte)),
("Seguro de vida", formata_moeda(entrada.seguro_vida)),
("Vale Alimentação", formata_moeda(entrada.vale_alimentacao)),
("Assiduidade", formata_moeda(entrada.assiduidade)),
("Fundo de Formação", formata_moeda(entrada.fundo_formacao)),
]
linhas += [(nome, formata_moeda(valor)) for nome, valor in entrada.outros_beneficios]
indice_subtotal_variaveis = len(linhas)
linhas.append(("Subtotal Custos Variáveis", formata_moeda(resultado.subtotal_custos_variaveis)))
indice_total = len(linhas)
linhas.append(("Custo Total Empresa", formata_moeda(resultado.custo_total_empresa)))
return _tabela_secao(
"CUSTO PARA O EMPREGADOR",
linhas,
indices_subtotal=frozenset({indice_subtotal_fixos, indice_subtotal_variaveis}),
indices_total=frozenset({indice_total}),
)
def _secao_liquido_empregado(resultado: ResultadoSimulacaoEmpregado) -> Table:
entrada = resultado.entrada
linhas = [
("Salário Bruto", formata_moeda(entrada.salario_contratual)),
("Adicional Art. 62.", formata_moeda(entrada.adicional_art62)),
("Comissão", formata_moeda(entrada.comissao)),
("DSR", formata_moeda(entrada.dsr)),
]
linhas += [(nome, formata_moeda(valor)) for nome, valor in entrada.outros_itens_remuneracao]
linhas += [
("Base INSS", formata_moeda(resultado.base_inss_empregado)),
("Alíquota INSS", formata_percentual(resultado.aliquota_inss_empregado)),
("Desconto INSS", formata_moeda(resultado.desconto_inss_empregado)),
("Base IRRF", formata_moeda(resultado.base_irrf)),
("Alíquota IRRF", formata_percentual(resultado.aliquota_irrf)),
("Desconto IRRF", formata_moeda(resultado.desconto_irrf)),
("Desconto Vale Transporte", formata_moeda(entrada.desconto_vale_transporte)),
("Desconto Vale Alimentação", formata_moeda(entrada.desconto_vale_alimentacao)),
("Desconto Sindicato", formata_moeda(entrada.desconto_sindicato)),
]
linhas += [(nome, formata_moeda(valor)) for nome, valor in entrada.outros_descontos]
indice_total = len(linhas)
linhas.append(("Valor Líquido do Empregado", formata_moeda(resultado.liquido_empregado)))
return _tabela_secao("VALOR LÍQUIDO DO EMPREGADO", linhas, indices_total=frozenset({indice_total}))
def gera_pdf_simulacao(resultado: ResultadoSimulacaoEmpregado, gerado_em: str) -> bytes:
buffer = io.BytesIO()
doc = SimpleDocTemplate(
buffer,
pagesize=A4,
topMargin=_ALTURA_BANNER + 0.9 * cm,
bottomMargin=2.1 * cm,
leftMargin=_MARGEM,
rightMargin=_MARGEM,
title="Simulação de Custo de Contratação",
)
story: list = [
_secao_dados_informados(resultado),
Spacer(1, 0.5 * cm),
_secao_custo_empregador(resultado),
Spacer(1, 0.5 * cm),
_secao_liquido_empregado(resultado),
]
def _desenha_pagina(canvas: Canvas, doc: SimpleDocTemplate) -> None:
_pagina(canvas, doc, resultado.entrada.nome_referencia, resultado.entrada.empresa, resultado.entrada.cargo, gerado_em)
doc.build(story, onFirstPage=_desenha_pagina, onLaterPages=_desenha_pagina)
return buffer.getvalue()

View File

@ -0,0 +1,46 @@
"""Valores padrão (seed) das tabelas de INSS/IRRF 2026, portados da planilha
de referência do escritório (`projects/planilha de custo/1972 - CUSTO
EMPREGADO SALÁRIO.xlsx`).
Estes valores só são usados para popular `ParametroFiscalCustoContratacao`
(`portal_api/models.py`) na primeira vez que essa linha é criada — depois
disso, quem manda é o que estiver salvo no banco (editável pela própria tela
de Simulação de Custo de Contratação, já que essas tabelas mudam todo ano).
Não há nenhuma fonte automática de atualização aqui de propósito (mesmo
espírito de `feriados_view`, que também não tenta inferir nada além do que
está documentado)."""
# Cada item é (limite_superior_da_base, aliquota, valor_a_deduzir).
FAIXAS_INSS: list[tuple[float, float, float]] = [
(1621.00, 0.075, 0.0),
(2902.84, 0.09, 24.32),
(4354.27, 0.12, 111.40),
(8475.55, 0.14, 198.49),
]
# Acima do limite da última faixa, o desconto do INSS é o teto fixo abaixo
# (a alíquota "nominal" nesse trecho não é usada em nenhuma outra fórmula).
TETO_DESCONTO_INSS: float = 988.09
FAIXAS_IRRF: list[tuple[float, float, float]] = [
(2428.80, 0.0, 0.0),
(2826.65, 0.075, 182.16),
(3751.05, 0.15, 394.16),
(4664.68, 0.225, 675.49),
]
# Acima do limite da última faixa.
ALIQUOTA_IRRF_TOPO: float = 0.275
DEDUZIR_IRRF_TOPO: float = 908.73
DESCONTO_SIMPLIFICADO_IRRF: float = 607.20
DEDUCAO_POR_DEPENDENTE: float = 189.59
# Redução adicional de IRRF da Lei nº 15.270/2025 (art. 3º-A da Lei nº
# 9.250/1995), vigente a partir de janeiro/2026 — isenção total até
# R$5.000,00 de rendimento bruto mensal, com redução decrescente até
# R$7.350,00 (zera a partir daí). Aplicada por cima do imposto já calculado
# pela tabela progressiva tradicional acima (essa lei não mudou as faixas
# em si), nunca deixando o imposto final negativo — ver
# `calculo.calcula_custo_empregado`.
REDUCAO_LEI_15270_COEFICIENTE_A: float = 978.62
REDUCAO_LEI_15270_COEFICIENTE_B: float = 0.133145
REDUCAO_LEI_15270_LIMITE: float = 7350.00

View File

View File

@ -0,0 +1,300 @@
"""Recálculo dos percentuais "atingido" e dos valores em R$ de um colaborador
de uma apuração — chamado tanto na criação da apuração (com os valores
automáticos do pipeline) quanto depois de qualquer ajuste manual de uma
`IndicadorApuracaoResposta` (individual ou em lote).
Convenção de escala: todo campo "percentual"/"pct_*" deste app guarda um
número de 0 a 100 (ex.: 90 = 90%), nunca uma fração — dividimos por 100 só no
ponto de uso, na fórmula de valor."""
from __future__ import annotations
from decimal import ROUND_HALF_UP, Decimal
from typing import Any
from portal_api.models import (
IndicadorApuracaoColaborador,
IndicadorCriterio,
IndicadorPercentualTipo,
)
VALORES_QUE_CONTAM_COMO_ATINGIDO = {"SIM"}
VALORES_EXCLUIDOS_DO_CALCULO = {"NAO_SE_APLICA"}
CEM = Decimal("100")
CENTAVO = Decimal("0.01")
def _fracao_atingida(resposta: Any) -> Decimal:
"""Critério automático (tem `percentual_calculado`) que bateu ou passou o
`limiar_percentual` do critério (a meta, ex.: 90%) conta crédito cheio —
bater a meta é bater a meta, não importa se foi por pouco (91,30% de uma
meta de 90%) ou com folga; não faz sentido pesar mais quem passou mais da
meta na média. Abaixo do limiar, conta o percentual real medido, mesmo
que o RH tenha marcado SIM por cima manualmente — forçar "Sim" num
critério que mediu 57% (abaixo da meta) não deve "arredondar" pra 100% na
média, senão o número perde sentido. Pra dar crédito cheio nesse caso
(abaixo da meta, mas o RH quer contar como atingido), o RH ajusta o
percentual agregado (individual/grupo/departamento) direto (ver
IndicadorApuracaoColaboradorViewSet/IndicadorApuracaoViewSet), não o
critério individual. Critério manual (sem percentual_calculado) segue
binário: SIM conta cheio, qualquer outro valor (NÃO/NÃO FAZ) conta como
falha."""
if resposta.percentual_calculado is not None:
if resposta.percentual_calculado >= resposta.criterio.limiar_percentual:
return Decimal("1")
return resposta.percentual_calculado / CEM
return Decimal("1") if resposta.valor in VALORES_QUE_CONTAM_COMO_ATINGIDO else Decimal("0")
def _respostas_consideradas(respostas: list) -> list:
return [resposta for resposta in respostas if resposta.valor not in VALORES_EXCLUIDOS_DO_CALCULO]
def _pct_atingido(respostas: list) -> Decimal:
"""Média ponderada pelo peso do critério (ver `_fracao_atingida` pra como
cada resposta contribui) — NÃO SE APLICA é excluído do denominador. Sem
nenhum critério aplicável, considera 100% (neutro) em vez de zerar o valor
de quem ainda não tem critério configurado pro grupo."""
consideradas = _respostas_consideradas(respostas)
peso_total = sum((resposta.criterio.peso for resposta in consideradas), Decimal("0"))
if peso_total == 0:
return CEM
peso_atingido = sum((resposta.criterio.peso * _fracao_atingida(resposta) for resposta in consideradas), Decimal("0"))
return (peso_atingido / peso_total) * CEM
def _peso_medio_nivel(respostas: list) -> Decimal:
"""Peso "do nível" (individual/grupo/departamento) inteiro, usado só pra
compor o percentual final do Indicador Individual (ver
`recalcula_colaborador`) — média do peso dos critérios considerados
nesse nível nesta competência (mesmo filtro de NÃO SE APLICA de
`_pct_atingido`). É média, não soma: o nível entra na composição final
como se ele mesmo fosse um "critério" só, com esse peso médio, ao lado
dos outros dois níveis — por isso não soma pesos de critérios paralelos
dentro do mesmo nível (ex.: os 3 critérios automáticos de Individual têm
peso 60 cada; o nível "Individual" entra na composição com peso 60, não
180). Sem nenhum critério aplicável, o nível não pesa nada (0) na
composição, em vez de distorcê-la com um peso arbitrário."""
consideradas = _respostas_consideradas(respostas)
if not consideradas:
return Decimal("0")
peso_total = sum((resposta.criterio.peso for resposta in consideradas), Decimal("0"))
return peso_total / len(consideradas)
def _combina_niveis(*niveis: tuple[Decimal, Decimal]) -> Decimal:
"""Combina (percentual, peso_do_nível) de Individual/Grupo/Departamento
num único percentual — média ponderada pelo peso de cada nível. Sem
nenhum nível com peso (caso extremo, nenhum critério cadastrado em lugar
nenhum), considera 100% (neutro), mesmo critério de `_pct_atingido`."""
peso_total = sum((peso for _, peso in niveis), Decimal("0"))
if peso_total == 0:
return CEM
peso_atingido = sum((pct * peso for pct, peso in niveis), Decimal("0"))
return peso_atingido / peso_total
def percentual_vigente(tipo: str, competencia, departamento_id: int | None) -> IndicadorPercentualTipo | None:
"""Sem `departamento_id` (colaborador cujo gerente não está mapeado a
nenhum departamento — ver `portal_api.indicadores.departamentos`), não há
o que buscar: a query com `departamento_id=None` sempre devolve vazio, já
que `IndicadorPercentualTipo.departamento` é obrigatório."""
return (
IndicadorPercentualTipo.objects.filter(
tipo=tipo, departamento_id=departamento_id, vigente_desde__lte=competencia
)
.order_by("-vigente_desde")
.first()
)
def respostas_aplicaveis(
colaborador: IndicadorApuracaoColaborador,
grupo: str,
respostas: list | None = None,
tipos_do_colaborador: set[str] | None = None,
) -> list:
"""Respostas do colaborador que contam pro nível `grupo` (individual/
grupo/departamento) nesta apuração — período compatível com a competência
e, só pra Individual, papel aplicável a algum dos tipos do colaborador.
Extraída de `recalcula_colaborador` pra ser reaproveitada também por
`composicao_individual` (exibição), sem duplicar o filtro. `respostas`/
`tipos_do_colaborador` podem ser passados prontos (quem chama em loop,
uma vez por nível, evita reconsultar `colaborador.empresas.all()` a cada
chamada)."""
if respostas is None:
respostas = list(colaborador.respostas.select_related("criterio"))
if tipos_do_colaborador is None:
tipos_do_colaborador = {empresa.tipo for empresa in colaborador.empresas.all()}
periodo_atual = colaborador.apuracao.periodo_criterio()
aplicaveis = []
for resposta in respostas:
criterio = resposta.criterio
if criterio.grupo != grupo:
continue
if criterio.periodo not in (IndicadorCriterio.PERIODO_TODOS, periodo_atual):
continue
if grupo == IndicadorCriterio.GRUPO_INDIVIDUAL and criterio.papel_aplicavel:
if criterio.papel_aplicavel not in tipos_do_colaborador:
continue
aplicaveis.append(resposta)
return aplicaveis
def composicao_individual(colaborador: IndicadorApuracaoColaborador) -> dict[str, Any]:
"""Detalhamento de como `pct_individual` (o "Total Indicador" exibido na
tela) foi composto a partir dos 3 níveis — só pra exibição (card de
revisão e recibo em PDF), reconstruído com a mesma lógica de
`recalcula_colaborador`, nunca usado pra recalcular nada. O percentual de
Individual aqui é o valor **bruto**, antes da composição (diferente de
`colaborador.pct_individual`, que já é o "Total"); Grupo/Departamento já
são os valores efetivos armazenados (automáticos ou ajustados
manualmente), os mesmos usados de fato na composição. Se
`pct_individual_ajustado_manualmente` for `True`, o "Total" não é a média
ponderada dos 3 níveis abaixo — foi sobrescrito manualmente pelo RH/pela
Direção (ver `ajustado_manualmente` no retorno). `total_calculado` é
sempre essa média ponderada, mesmo quando `total` foi sobrescrito —
é o que o recibo em PDF mostra no banner principal (o percentual que o
colaborador de fato atingiu), com o valor ajustado aparecendo à parte,
já rotulado como decisão da Direção.
Usa `.all()` (não `.select_related("criterio")`) de propósito — chamada
uma vez por colaborador ao serializar uma apuração inteira, precisa
reaproveitar o `prefetch_related("colaboradores__respostas__criterio")`
de `IndicadorApuracaoViewSet.get_queryset` (que já resolve `criterio` sem
query extra); `.select_related()` criaria uma queryset nova e ignoraria
esse cache, voltando a consultar o banco uma vez por colaborador."""
respostas = list(colaborador.respostas.all())
tipos_do_colaborador = {empresa.tipo for empresa in colaborador.empresas.all()}
respostas_individual = respostas_aplicaveis(
colaborador, IndicadorCriterio.GRUPO_INDIVIDUAL, respostas, tipos_do_colaborador
)
respostas_grupo = respostas_aplicaveis(colaborador, IndicadorCriterio.GRUPO_GRUPO, respostas, tipos_do_colaborador)
respostas_departamento = respostas_aplicaveis(
colaborador, IndicadorCriterio.GRUPO_DEPARTAMENTO, respostas, tipos_do_colaborador
)
percentual_individual_bruto = _pct_atingido(respostas_individual)
peso_individual = _peso_medio_nivel(respostas_individual)
peso_grupo = _peso_medio_nivel(respostas_grupo)
peso_departamento = _peso_medio_nivel(respostas_departamento)
return {
"individual": {"percentual": percentual_individual_bruto, "peso": peso_individual},
"grupo": {"percentual": colaborador.pct_grupo, "peso": peso_grupo},
"departamento": {"percentual": colaborador.pct_departamento, "peso": peso_departamento},
"total": colaborador.pct_individual,
# Igual a `total`, exceto quando `pct_individual_ajustado_manualmente`
# é True — nesse caso `total` é o valor que a Direção decidiu pagar,
# e `total_calculado` é o percentual que o colaborador de fato mediu
# (a mesma composição que `recalcula_colaborador` teria gravado se
# não houvesse ajuste manual). Usado no recibo em PDF pra mostrar os
# dois números lado a lado (ver CLAUDE.md).
"total_calculado": _combina_niveis(
(percentual_individual_bruto, peso_individual),
(colaborador.pct_grupo, peso_grupo),
(colaborador.pct_departamento, peso_departamento),
),
"ajustado_manualmente": colaborador.pct_individual_ajustado_manualmente,
}
def recalcula_colaborador(colaborador: IndicadorApuracaoColaborador) -> None:
apuracao = colaborador.apuracao
respostas = list(colaborador.respostas.select_related("criterio"))
tipos_do_colaborador = {empresa.tipo for empresa in colaborador.empresas.all()}
respostas_individual = respostas_aplicaveis(
colaborador, IndicadorCriterio.GRUPO_INDIVIDUAL, respostas, tipos_do_colaborador
)
respostas_grupo = respostas_aplicaveis(colaborador, IndicadorCriterio.GRUPO_GRUPO, respostas, tipos_do_colaborador)
respostas_departamento = respostas_aplicaveis(
colaborador, IndicadorCriterio.GRUPO_DEPARTAMENTO, respostas, tipos_do_colaborador
)
# Grupo/Departamento continuam sendo só a média dos próprios critérios
# (cada um só ajustado manualmente em bloco — ver IndicadorApuracaoViewSet
# .ajustar_grupo/ajustar_departamento, não aqui). Precisam ser calculados
# ANTES de Individual, que os usa na composição final abaixo.
if not colaborador.pct_grupo_ajustado_manualmente:
colaborador.pct_grupo = _pct_atingido(respostas_grupo)
if not colaborador.pct_departamento_ajustado_manualmente:
colaborador.pct_departamento = _pct_atingido(respostas_departamento)
# Individual é o percentual final do colaborador — não é só a média dos
# critérios de nível Individual, é a composição dos 3 níveis (Individual/
# Grupo/Departamento), cada um pesando conforme o peso dos seus próprios
# critérios (ver _peso_medio_nivel/_combina_niveis). Ex.: Individual com
# critérios peso 60 atingindo 57,14%, Grupo peso 10 atingindo 100% e
# Departamento peso 30 atingindo 100% resulta em (57,14×60 + 100×10 +
# 100×30) / (60+10+30) = 74,28% — decisão explícita do usuário, ver
# CLAUDE.md. Usa colaborador.pct_grupo/pct_departamento já calculados
# acima (efetivos, sejam eles automáticos ou ajustados manualmente).
if not colaborador.pct_individual_ajustado_manualmente:
colaborador.pct_individual = _combina_niveis(
(_pct_atingido(respostas_individual), _peso_medio_nivel(respostas_individual)),
(colaborador.pct_grupo, _peso_medio_nivel(respostas_grupo)),
(colaborador.pct_departamento, _peso_medio_nivel(respostas_departamento)),
)
percentuais_por_tipo = {
tipo: percentual_vigente(tipo, apuracao.competencia, colaborador.departamento_id)
for tipo in tipos_do_colaborador
}
valor_total_colaborador = Decimal("0")
for empresa in colaborador.empresas.all():
percentuais = percentuais_por_tipo.get(empresa.tipo)
honorario_ajustado = (empresa.honorario * (colaborador.pct_individual / CEM)).quantize(
CENTAVO, rounding=ROUND_HALF_UP
)
empresa.honorario_ajustado = honorario_ajustado
if percentuais is None:
empresa.valor_individual = Decimal("0")
empresa.valor_grupo = Decimal("0")
empresa.valor_departamento = Decimal("0")
else:
valor_individual = honorario_ajustado * (percentuais.percentual_individual / CEM)
valor_grupo = valor_individual * (percentuais.percentual_grupo / CEM) * (colaborador.pct_grupo / CEM)
valor_departamento = (
valor_individual
* (percentuais.percentual_departamento / CEM)
* (colaborador.pct_departamento / CEM)
)
empresa.valor_individual = valor_individual.quantize(CENTAVO, rounding=ROUND_HALF_UP)
empresa.valor_grupo = valor_grupo.quantize(CENTAVO, rounding=ROUND_HALF_UP)
empresa.valor_departamento = valor_departamento.quantize(CENTAVO, rounding=ROUND_HALF_UP)
empresa.valor_total = empresa.valor_individual + empresa.valor_grupo + empresa.valor_departamento
empresa.save(
update_fields=["honorario_ajustado", "valor_individual", "valor_grupo", "valor_departamento", "valor_total"]
)
valor_total_colaborador += empresa.valor_total
colaborador.valor_total = valor_total_colaborador
colaborador.save(update_fields=["pct_individual", "pct_grupo", "pct_departamento", "valor_total"])
def limpa_ajuste_individual(colaborador: IndicadorApuracaoColaborador) -> None:
"""Reverte só o percentual Individual pro modo automático (usado pela
action `recalcular` de IndicadorApuracaoColaboradorViewSet) — não
recalcula por si só, quem chama ainda precisa rodar `recalcula_colaborador`
depois pra repor o valor calculado."""
colaborador.pct_individual_ajustado_manualmente = False
colaborador.save(update_fields=["pct_individual_ajustado_manualmente"])
def limpa_ajuste_grupo(colaborador: IndicadorApuracaoColaborador) -> None:
"""Reverte só o percentual Grupo pro modo automático — chamado uma vez por
colaborador do mesmo gerente pela action `recalcular_grupo` de
IndicadorApuracaoViewSet (Grupo é editado/revertido em bloco por gerente,
não colaborador a colaborador — ver CLAUDE.md)."""
colaborador.pct_grupo_ajustado_manualmente = False
colaborador.save(update_fields=["pct_grupo_ajustado_manualmente"])
def limpa_ajuste_departamento(colaborador: IndicadorApuracaoColaborador) -> None:
"""Reverte só o percentual Departamento pro modo automático — chamado uma
vez por colaborador da apuração pela action `recalcular_departamento` de
IndicadorApuracaoViewSet (Departamento é editado/revertido em bloco pra
toda a apuração, não colaborador a colaborador — ver CLAUDE.md)."""
colaborador.pct_departamento_ajustado_manualmente = False
colaborador.save(update_fields=["pct_departamento_ajustado_manualmente"])

View File

@ -0,0 +1,31 @@
"""Resolução de departamento por gerente — substitui o antigo módulo
`setores.py` (coluna bruta "departamento" da planilha Tareffa + fusão
automática Contabilidade/Fiscal→Fisco-Contábil + `IndicadorSetorApelido`).
O departamento de um colaborador agora vem de `IndicadorDepartamentoGerente`
(nome do gerente → `IndicadorDepartamento`), um cadastro mantido pela própria
aplicação (Configurações → Departamentos) — decisão explícita do usuário, que
achou mais simples e robusto relacionar gerência↔departamento manualmente do
que continuar inferindo de um texto livre vindo da planilha. Isso também
resolve de graça o caso que o antigo `IndicadorSetorApelido` cobria (os 4
líderes de Fisco/Contábil, cujo próprio setor bruto na planilha não batia com
o departamento que deveriam mostrar): basta mapear a gerente deles,
"Elizangela de Paula Kuhn", pro departamento "Gerentes" — todo colaborador
com esse `gerente` cai lá automaticamente, sem apelido por pessoa."""
from __future__ import annotations
from portal_api.models import IndicadorDepartamentoGerente
def carrega_mapa_gerentes() -> dict[str, int]:
"""{nome_gerente: departamento_id} — uma consulta só, reaproveitada pra
resolver o departamento de todos os colaboradores de uma apuração de
uma vez (ver `IndicadorApuracaoViewSet.create`)."""
return dict(IndicadorDepartamentoGerente.objects.values_list("nome_gerente", "departamento_id"))
def departamento_id_do_gerente(nome_gerente: str, mapa: dict[str, int] | None = None) -> int | None:
if mapa is None:
mapa = carrega_mapa_gerentes()
return mapa.get(nome_gerente)

View File

@ -0,0 +1,41 @@
"""% de entregas no prazo de um responsável, pro cálculo automático dos 3
critérios individuais (Balancete/Liberação Fiscal/Conciliação Financeira) —
replica a lógica da aba "5-RESULTADO META" da planilha antiga."""
from __future__ import annotations
from dataclasses import dataclass
from decimal import Decimal
from .leiaute import STATUS_CANCELADO, LinhaTareffa
@dataclass
class PercentualEntrega:
total: int
atrasados: int
em_branco: int
percentual: Decimal | None # None quando total == 0 (responsável não presta esse serviço)
def calcula_percentual_entrega(linhas: list[LinhaTareffa], responsavel: str, servicos: set[str]) -> PercentualEntrega:
"""Atrasado = entregue depois da data meta; em branco = ainda sem data de
entrega (serviço aberto). `percentual = (total - atrasados - em_branco) / total`."""
relevantes = [
linha
for linha in linhas
if linha.responsavel == responsavel and linha.servico in servicos and linha.status != STATUS_CANCELADO
]
total = len(relevantes)
if total == 0:
return PercentualEntrega(total=0, atrasados=0, em_branco=0, percentual=None)
em_branco = sum(1 for linha in relevantes if linha.data_entrega is None)
atrasados = sum(
1
for linha in relevantes
if linha.data_entrega is not None and linha.data_meta is not None and linha.data_entrega > linha.data_meta
)
no_prazo = total - atrasados - em_branco
percentual = (Decimal(no_prazo) / Decimal(total)) * Decimal(100)
return PercentualEntrega(total=total, atrasados=atrasados, em_branco=em_branco, percentual=percentual)

View File

@ -0,0 +1,120 @@
"""Leitura das duas planilhas mensais do Indicador de Desempenho (Fiscontábil):
"Serviços Tareffa" (uma linha por serviço executado) e "Honorários Por
Cliente" (honorário líquido por empresa). Pacote Python puro, sem depender
do ORM — mesmo espírito de `portal_api.planos_saude`."""
from __future__ import annotations
import datetime as dt
from dataclasses import dataclass
from decimal import Decimal, InvalidOperation
import openpyxl
SERVICOS_BALANCETE = {"Balancete Mensal", "Balancete Bimestral"}
SERVICO_LIBERACAO_FISCAL = "Liberação Fiscal"
SERVICO_CONCILIACAO_FINANCEIRA = "Conciliação Financeira"
SERVICOS_INDICADOR = SERVICOS_BALANCETE | {SERVICO_LIBERACAO_FISCAL, SERVICO_CONCILIACAO_FINANCEIRA}
STATUS_CANCELADO = "Cancelado"
@dataclass
class LinhaTareffa:
codigo_empresa: str
razao_social: str
responsavel: str
gerente: str
servico: str
status: str
data_meta: dt.date | None
data_entrega: dt.date | None
def _texto(valor: object) -> str:
return str(valor).strip() if valor is not None else ""
def _codigo(valor: object) -> str:
"""Normaliza um código de empresa pra string, tratando o caso comum de o
Excel/openpyxl devolver um float "inteiro" (ex.: 792.0) — sem isso, o
mesmo código viria como "792" de uma planilha e "792.0" de outra, e a
junção por código nunca bateria."""
if isinstance(valor, float) and valor.is_integer():
return str(int(valor))
return _texto(valor)
def _para_data(valor: object) -> dt.date | None:
if isinstance(valor, dt.datetime):
return valor.date()
if isinstance(valor, dt.date):
return valor
return None
def _indice_cabecalho(planilha) -> dict[str, int]:
primeira_linha = next(planilha.iter_rows(min_row=1, max_row=1, values_only=True))
return {_texto(nome): posicao for posicao, nome in enumerate(primeira_linha)}
def le_servicos_tareffa(caminho: str) -> list[LinhaTareffa]:
"""Lê só as linhas relevantes pro Indicador de Desempenho do Fiscontábil
(Balancete/Liberação Fiscal/Conciliação Financeira) — outros serviços
(ex.: departamentos "Rocket"/"Condomínio"/"Pessoa Física IRPF", fora do
escopo desta v1) são ignorados aqui, não em cada consumidor."""
workbook = openpyxl.load_workbook(caminho, data_only=True, read_only=True)
try:
planilha = workbook.worksheets[0]
indice = _indice_cabecalho(planilha)
linhas: list[LinhaTareffa] = []
for linha_planilha in planilha.iter_rows(min_row=2, values_only=True):
servico = _texto(linha_planilha[indice["servico"]])
if servico not in SERVICOS_INDICADOR:
continue
linhas.append(
LinhaTareffa(
codigo_empresa=_codigo(linha_planilha[indice["codigoempresa"]]),
razao_social=_texto(linha_planilha[indice["razaosocial"]]),
responsavel=_texto(linha_planilha[indice["responsavel"]]),
gerente=_texto(linha_planilha[indice["gerente"]]),
servico=servico,
status=_texto(linha_planilha[indice["status"]]),
data_meta=_para_data(linha_planilha[indice["datameta"]]),
data_entrega=_para_data(linha_planilha[indice["dataentrega"]]),
)
)
return linhas
finally:
# Modo read_only do openpyxl mantém o arquivo aberto (memory-mapped)
# até `close()` ser chamado — sem isso, o Windows bloqueia qualquer
# tentativa de apagar o upload depois (ex.: excluir a apuração).
workbook.close()
def le_honorarios(caminho: str) -> dict[str, Decimal]:
"""Devolve {código da empresa: honorário}, somando quando o mesmo código
aparecer em mais de uma linha (mais de um serviço faturado pro mesmo
cliente no mês)."""
workbook = openpyxl.load_workbook(caminho, data_only=True, read_only=True)
try:
planilha = workbook.worksheets[0]
indice = _indice_cabecalho(planilha)
coluna_codigo = indice["Cód"]
coluna_valor = indice["Total Líquido"]
honorarios: dict[str, Decimal] = {}
for linha_planilha in planilha.iter_rows(min_row=2, values_only=True):
codigo = _codigo(linha_planilha[coluna_codigo])
if not codigo:
continue
valor_bruto = linha_planilha[coluna_valor]
try:
valor = Decimal(str(valor_bruto)) if valor_bruto is not None else Decimal("0")
except InvalidOperation:
valor = Decimal("0")
honorarios[codigo] = honorarios.get(codigo, Decimal("0")) + valor
return honorarios
finally:
workbook.close()

View File

@ -0,0 +1,151 @@
"""Orquestração do Indicador de Desempenho: lê as duas planilhas, deriva os
papéis/tipos por empresa e pré-calcula os 3 critérios automáticos por
colaborador. Só calcula em memória (dataclasses), sem tocar no ORM — mesmo
padrão de `portal_api.planos_saude.pipeline`; quem persiste é a view."""
from __future__ import annotations
from dataclasses import dataclass, field
from decimal import Decimal
from .entregas import calcula_percentual_entrega
from .leiaute import (
SERVICO_CONCILIACAO_FINANCEIRA,
SERVICO_LIBERACAO_FISCAL,
SERVICOS_BALANCETE,
le_honorarios,
le_servicos_tareffa,
)
from .tipos import PapelPorEmpresa, deriva_tipos_por_empresa
CALCULO_BALANCETE = "balancete"
CALCULO_LIBERACAO_FISCAL = "liberacao_fiscal"
CALCULO_CONCILIACAO = "conciliacao"
SERVICOS_POR_CALCULO_AUTOMATICO = {
CALCULO_BALANCETE: SERVICOS_BALANCETE,
CALCULO_LIBERACAO_FISCAL: {SERVICO_LIBERACAO_FISCAL},
CALCULO_CONCILIACAO: {SERVICO_CONCILIACAO_FINANCEIRA},
}
@dataclass
class CriterioAutomaticoConfig:
criterio_id: int
tipo_calculo: str # "balancete" | "liberacao_fiscal" | "conciliacao"
limiar_percentual: Decimal
@dataclass
class RespostaAutomatica:
criterio_id: int
valor: str # "SIM" | "NAO" | "NAO_FAZ"
percentual_calculado: Decimal | None
@dataclass
class EmpresaCalculada:
codigo_empresa: str
nome_empresa: str
honorario: Decimal
honorario_nao_encontrado: bool
tipo: str
@dataclass
class ColaboradorCalculado:
nome: str
gerente: str
departamento_id: int | None = None
empresas: list[EmpresaCalculada] = field(default_factory=list)
respostas_automaticas: list[RespostaAutomatica] = field(default_factory=list)
@dataclass
class ResultadoApuracao:
colaboradores: list[ColaboradorCalculado]
avisos: list[str]
def processa_apuracao(
caminho_tareffa: str,
caminho_honorarios: str,
gerente_departamento: dict[str, int],
criterios_automaticos_por_departamento: dict[int, list[CriterioAutomaticoConfig]],
) -> ResultadoApuracao:
"""`gerente_departamento` ({nome do gerente: id do IndicadorDepartamento})
e `criterios_automaticos_por_departamento` ({id do departamento: lista de
critérios automáticos daquele departamento}) vêm prontos de fora (ORM
fica só na view) — cada colaborador é resolvido pro departamento do seu
`gerente` antes de decidir quais critérios automáticos calcular pra ele,
já que cada departamento pode ter critérios/pesos/limiares diferentes.
Colaborador cujo gerente não está em `gerente_departamento` fica com
`departamento_id=None` e sem nenhum critério automático calculado (vira
aviso, ver `avisos` abaixo)."""
linhas = le_servicos_tareffa(caminho_tareffa)
honorarios = le_honorarios(caminho_honorarios)
derivacao = deriva_tipos_por_empresa(linhas)
avisos = list(derivacao.avisos)
gerente_por_responsavel: dict[str, str] = {}
for linha in linhas:
if linha.responsavel and linha.responsavel not in gerente_por_responsavel:
gerente_por_responsavel[linha.responsavel] = linha.gerente
papeis_por_responsavel: dict[str, list[PapelPorEmpresa]] = {}
for papel in derivacao.papeis:
papeis_por_responsavel.setdefault(papel.responsavel, []).append(papel)
colaboradores: list[ColaboradorCalculado] = []
for responsavel, papeis in sorted(papeis_por_responsavel.items()):
gerente = gerente_por_responsavel.get(responsavel, "")
departamento_id = gerente_departamento.get(gerente)
if departamento_id is None:
avisos.append(
f'Gerente "{gerente or "(em branco)"}" (colaborador "{responsavel}") não está associado a '
"nenhum departamento — cadastre essa relação em Configurações → Departamentos; até lá, este "
"colaborador fica sem critérios nem percentuais calculados."
)
colaborador = ColaboradorCalculado(nome=responsavel, gerente=gerente, departamento_id=departamento_id)
for papel in papeis:
honorario = honorarios.get(papel.codigo_empresa)
colaborador.empresas.append(
EmpresaCalculada(
codigo_empresa=papel.codigo_empresa,
nome_empresa=papel.nome_empresa,
honorario=honorario if honorario is not None else Decimal("0"),
honorario_nao_encontrado=honorario is None,
tipo=papel.tipo,
)
)
criterios_automaticos = criterios_automaticos_por_departamento.get(departamento_id, [])
for criterio in criterios_automaticos:
servicos = SERVICOS_POR_CALCULO_AUTOMATICO.get(criterio.tipo_calculo)
if not servicos:
continue
resultado_entrega = calcula_percentual_entrega(linhas, responsavel, servicos)
if resultado_entrega.percentual is None:
# Responsável nunca prestou esse tipo de serviço na competência —
# o critério não se aplica a ele, não é uma falha (isso é o que
# permite deixar `papel_aplicavel` em branco nos 3 critérios
# automáticos: quem não faz aquele serviço fica de fora do
# cálculo por conta própria, sem precisar restringir por papel).
valor = "NAO_SE_APLICA"
elif resultado_entrega.percentual >= criterio.limiar_percentual:
valor = "SIM"
else:
valor = "NAO"
colaborador.respostas_automaticas.append(
RespostaAutomatica(
criterio_id=criterio.criterio_id,
valor=valor,
percentual_calculado=resultado_entrega.percentual,
)
)
colaboradores.append(colaborador)
return ResultadoApuracao(colaboradores=colaboradores, avisos=avisos)

View File

@ -0,0 +1,472 @@
"""Geração do recibo em PDF de um colaborador, a partir dos dados já
calculados/revisados (não reprocessa nada — só formata o que está salvo).
Layout inspirado na planilha antiga do escritório (ver anexo do usuário),
com a identidade visual do Portal (logo + paleta roxo/dourado, ver
tokens.css) — mesma paleta do tema claro, já que o PDF é sempre impresso
em fundo branco."""
from __future__ import annotations
import calendar
import io
import threading
from decimal import Decimal
from django.conf import settings
from PIL import Image as PILImage
from reportlab.lib import colors
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
from reportlab.lib.units import cm
from reportlab.platypus import Image, KeepTogether, Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle
from portal_api.indicadores.calculo import composicao_individual, percentual_vigente
from portal_api.models import (
RESPOSTA_CRITERIO_INDICADOR_CHOICES,
IndicadorApuracao,
IndicadorApuracaoColaborador,
IndicadorCriterio,
TIPO_COLABORADOR_INDICADOR_CHOICES,
)
# Versão curta só pra coluna "Tipo" da tabela de Empresas — o label completo
# ("Contador (com conciliador)") não cabe na coluna estreita mesmo já
# quebrando linha (Paragraph); mesma abreviação "Contador SC/CC" já usada
# informalmente pra esses dois tipos em CLAUDE.md.
TIPO_LABEL_CURTO = {
"1": "Contábil+Fiscal",
"2": "Contador SC",
"3": "Contador CC",
"4": "Fiscal",
"5": "Conciliador",
}
RESPOSTA_LABEL = dict(RESPOSTA_CRITERIO_INDICADOR_CHOICES)
PERIODO_LABEL = {
IndicadorCriterio.PERIODO_TODOS: "Todos os meses",
IndicadorCriterio.PERIODO_MAR_A_NOV: "Março a Novembro",
IndicadorCriterio.PERIODO_DEZ_A_JAN: "Dezembro a Janeiro",
}
MES_LABEL = {
1: "janeiro", 2: "fevereiro", 3: "março", 4: "abril", 5: "maio", 6: "junho",
7: "julho", 8: "agosto", 9: "setembro", 10: "outubro", 11: "novembro", 12: "dezembro",
}
CIDADE_ESCRITORIO = "Foz do Iguaçu"
LOGO_PATH = settings.BASE_DIR / "static" / "img" / "logo.png"
# `logo.png` é 6250×4419px (~480KB) — pensado pra tela, não pra um logo de
# ~3,6cm impresso num PDF. Embutir o arquivo original em cada recibo (um ZIP
# chega a ter 40+ colaboradores) inflava o ZIP pra ~19MB e deixava o download
# lento/travado no navegador. Redimensiona uma vez por processo (cache em
# memória, protegido por lock — vários recibos podem ser gerados em paralelo
# dentro do mesmo worker) pra um tamanho generoso pra impressão (600px de
# largura, ~423 DPI num logo de 3,6cm) e reaproveita os bytes já reduzidos em
# todos os recibos daquele ZIP.
_logo_bytes_cache: bytes | None = None
_logo_bytes_lock = threading.Lock()
def _logo_bytes() -> bytes | None:
global _logo_bytes_cache
if _logo_bytes_cache is not None:
return _logo_bytes_cache or None
with _logo_bytes_lock:
if _logo_bytes_cache is None:
try:
imagem = PILImage.open(LOGO_PATH)
imagem.thumbnail((600, 600))
buffer = io.BytesIO()
imagem.save(buffer, format="PNG", optimize=True)
_logo_bytes_cache = buffer.getvalue()
except Exception:
_logo_bytes_cache = b""
return _logo_bytes_cache or None
# Paleta do escritório (mesmos valores do tema claro em tokens.css) — o PDF é
# sempre impresso/visualizado em fundo branco, então usa as cores pensadas
# pra contraste em fundo claro, não as do tema escuro (padrão da tela).
COR_ROXO_ESCURO = colors.HexColor("#3d2178")
COR_ROXO_CLARO = colors.HexColor("#efe8ff")
COR_DOURADO = colors.HexColor("#b4872a")
COR_DOURADO_CLARO = colors.HexColor("#f6ecd4")
COR_TEXTO = colors.HexColor("#211d2b")
COR_TEXTO_MUTED = colors.HexColor("#5f5a6d")
COR_BORDA = colors.HexColor("#e4e0ec")
COR_SUCESSO_BG = colors.HexColor("#dcf5e3")
COR_SUCESSO_TEXTO = colors.HexColor("#1a7a3d")
COR_FALHA_BG = colors.HexColor("#fbe2e2")
COR_FALHA_TEXTO = colors.HexColor("#b3261e")
COR_NEUTRO_BG = colors.HexColor("#ece9f2")
COR_NEUTRO_TEXTO = colors.HexColor("#5f5a6d")
COR_ATINGIDO_BG = {
"SIM": COR_SUCESSO_BG,
"NAO": COR_FALHA_BG,
"NAO_FAZ": COR_FALHA_BG,
"NAO_SE_APLICA": COR_NEUTRO_BG,
}
COR_ATINGIDO_TEXTO = {
"SIM": COR_SUCESSO_TEXTO,
"NAO": COR_FALHA_TEXTO,
"NAO_FAZ": COR_FALHA_TEXTO,
"NAO_SE_APLICA": COR_NEUTRO_TEXTO,
}
def _moeda(valor: Decimal) -> str:
# `&nbsp;` (não `" "`) entre "R$" e o número — todo valor aqui vira
# Paragraph numa coluna estreita, e um espaço normal é um ponto de quebra
# de linha válido pro word-wrap do reportlab: com espaço comum, um valor
# que não coubesse numa linha só quebrava bem ali, deixando "R$" sozinho
# numa linha acima do número. Não-breaking space mantém os dois sempre
# juntos, movendo a quebra (se precisar) pra dentro do próprio número.
texto = f"{valor:,.2f}"
return "R$&nbsp;" + texto.replace(",", "_").replace(".", ",").replace("_", ".")
def _percentual(valor: Decimal) -> str:
texto = f"{Decimal(valor):,.2f}"
return texto.replace(",", "_").replace(".", ",").replace("_", ".") + "%"
def _respostas_aplicaveis(colaborador: IndicadorApuracaoColaborador, periodo_atual: str) -> list:
"""Mesma regra de aplicabilidade de `calculo._respostas_do_grupo` (período
da competência + papel do colaborador pros critérios Individual) — só que
aqui olhando os 3 níveis juntos (Individual/Grupo/Departamento), pra
reproduzir o agrupamento por período da planilha antiga (ver docstring do
módulo)."""
tipos_do_colaborador = {empresa.tipo for empresa in colaborador.empresas.all()}
aplicaveis = []
for resposta in colaborador.respostas.all():
criterio = resposta.criterio
if criterio.periodo not in (IndicadorCriterio.PERIODO_TODOS, periodo_atual):
continue
if criterio.grupo == IndicadorCriterio.GRUPO_INDIVIDUAL and criterio.papel_aplicavel:
if criterio.papel_aplicavel not in tipos_do_colaborador:
continue
aplicaveis.append(resposta)
return aplicaveis
def _agrupa_por_periodo(respostas: list) -> list[tuple[str, list]]:
""""Todos os meses" sempre primeiro, depois o período sazonal ativo —
mesma ordem da planilha antiga."""
grupos: dict[str, list] = {}
for resposta in respostas:
grupos.setdefault(resposta.criterio.periodo, []).append(resposta)
ordem = [IndicadorCriterio.PERIODO_TODOS] + [
periodo for periodo in grupos if periodo != IndicadorCriterio.PERIODO_TODOS
]
return [(periodo, grupos[periodo]) for periodo in ordem if periodo in grupos]
def _estilos():
base = getSampleStyleSheet()
return {
"titulo": ParagraphStyle(
"ind_titulo", parent=base["Heading1"], alignment=TA_LEFT, fontSize=13,
textColor=COR_ROXO_ESCURO, spaceAfter=2,
),
"subtitulo": ParagraphStyle(
"ind_subtitulo", parent=base["Normal"], alignment=TA_LEFT, fontSize=9, textColor=COR_TEXTO_MUTED,
),
"normal": ParagraphStyle("ind_normal", parent=base["Normal"], fontSize=9, textColor=COR_TEXTO),
"secao": ParagraphStyle(
"ind_secao", parent=base["Heading2"], fontSize=10.5, textColor=COR_ROXO_ESCURO,
spaceBefore=10, spaceAfter=4,
),
"banner_label": ParagraphStyle(
"ind_banner_label", parent=base["Normal"], fontSize=9, textColor=colors.white, alignment=TA_LEFT,
),
"banner_valor": ParagraphStyle(
"ind_banner_valor", parent=base["Normal"], fontSize=20, textColor=colors.white,
fontName="Helvetica-Bold", alignment=TA_RIGHT,
),
"rodape": ParagraphStyle("ind_rodape", parent=base["Normal"], fontSize=9, alignment=TA_CENTER, textColor=COR_TEXTO),
"celula": ParagraphStyle("ind_celula", parent=base["Normal"], fontSize=7.2, textColor=COR_TEXTO, leading=8.6),
"celula_centro": ParagraphStyle(
"ind_celula_centro", parent=base["Normal"], fontSize=7.2, leading=8.6,
textColor=COR_TEXTO, alignment=TA_CENTER,
),
"celula_direita": ParagraphStyle(
"ind_celula_direita", parent=base["Normal"], fontSize=7.2, leading=8.6,
textColor=COR_TEXTO, alignment=TA_RIGHT,
),
"celula_negrito": ParagraphStyle(
"ind_celula_negrito", parent=base["Normal"], fontSize=7.6, leading=9,
textColor=COR_TEXTO, fontName="Helvetica-Bold",
),
"celula_direita_negrito": ParagraphStyle(
"ind_celula_direita_negrito", parent=base["Normal"], fontSize=7.6, leading=9,
textColor=COR_TEXTO, fontName="Helvetica-Bold", alignment=TA_RIGHT,
),
"cabecalho_tabela": ParagraphStyle(
"ind_cabecalho_tabela", parent=base["Normal"], fontSize=7.2, leading=8.4,
textColor=colors.white, fontName="Helvetica-Bold", alignment=TA_CENTER,
),
}
def _cabecalho(apuracao: IndicadorApuracao, estilos: dict) -> Table:
logo_bytes = _logo_bytes()
if logo_bytes:
logo = Image(io.BytesIO(logo_bytes), width=3.6 * cm, height=3.6 * cm / 1.41)
else:
logo = Paragraph("De Paula Contadores", estilos["titulo"])
titulo = [
Paragraph("PLANILHA DE APURAÇÃO DO INDICADOR DE DESEMPENHO", estilos["titulo"]),
Paragraph("Fiscontábil — Geradoc", estilos["subtitulo"]),
Paragraph(f"Competência: {apuracao.competencia:%m/%Y}", estilos["subtitulo"]),
]
tabela = Table([[logo, titulo]], colWidths=[4 * cm, None])
tabela.setStyle(
TableStyle(
[
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("ALIGN", (0, 0), (0, 0), "LEFT"),
("LINEBELOW", (0, 0), (-1, 0), 1.5, COR_DOURADO),
("BOTTOMPADDING", (0, 0), (-1, -1), 8),
]
)
)
return tabela
def _banner_percentual(colaborador: IndicadorApuracaoColaborador, estilos: dict) -> Table:
# O banner sempre mostra o percentual **efetivo/medido** (a composição
# dos 3 níveis), independente de qualquer ajuste manual — o valor que a
# Direção decidiu pagar (quando diferente) aparece à parte, na linha de
# detalhe abaixo, pra deixar claro os dois números: o que o colaborador
# atingiu e o que foi de fato considerado.
composicao = composicao_individual(colaborador)
percentual_medido = composicao["total_calculado"]
linha_titulo = Table(
[
[
Paragraph("PERCENTUAL DO INDICADOR INDIVIDUAL", estilos["banner_label"]),
Paragraph(_percentual(percentual_medido), estilos["banner_valor"]),
]
],
colWidths=[None, 5 * cm],
)
linha_titulo.setStyle(
TableStyle(
[
("BACKGROUND", (0, 0), (-1, -1), COR_ROXO_ESCURO),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("LEFTPADDING", (0, 0), (0, 0), 12),
("RIGHTPADDING", (-1, 0), (-1, 0), 12),
("TOPPADDING", (0, 0), (-1, -1), 8),
("BOTTOMPADDING", (0, 0), (-1, -1), 8),
]
)
)
linhas_detalhe = [
f"Grupo: <b>{_percentual(colaborador.pct_grupo)}</b>{' (ajustado manualmente)' if colaborador.pct_grupo_ajustado_manualmente else ''}",
f"Departamento: <b>{_percentual(colaborador.pct_departamento)}</b>{' (ajustado manualmente)' if colaborador.pct_departamento_ajustado_manualmente else ''}",
]
if colaborador.pct_individual_ajustado_manualmente:
linhas_detalhe.insert(
0, f"Percentual individual ajustado pela direção: <b>{_percentual(colaborador.pct_individual)}</b>."
)
subtexto = Paragraph(" &nbsp;·&nbsp; ".join(linhas_detalhe), estilos["normal"])
return KeepTogether([linha_titulo, Spacer(1, 0.15 * cm), subtexto])
def _tabela_empresas(colaborador: IndicadorApuracaoColaborador, estilos: dict) -> Table:
# Toda célula (menos o cabeçalho, já em Paragraph) precisa ser um
# Paragraph, não uma string solta — string solta não quebra linha dentro
# da coluna, e com ALIGN à direita/centro um valor mais largo que a
# coluna (ex.: "Contador (com conciliador)" em Tipo, ou os totais em
# negrito, que ficam mais largos que a mesma string em peso normal)
# simplesmente vaza por cima da célula vizinha em vez de quebrar linha.
empresas = list(colaborador.empresas.all())
cabecalho = ["Código", "Empresa", "Honorário", "Hon. Ajustado", "Tipo", "Indiv.", "Grupo", "Depto.", "Total"]
dados = [[Paragraph(texto, estilos["cabecalho_tabela"]) for texto in cabecalho]]
for empresa in empresas:
nome = empresa.nome_empresa or empresa.codigo_empresa
if empresa.honorario_nao_encontrado:
nome += " (honorário não encontrado)"
dados.append(
[
Paragraph(empresa.codigo_empresa, estilos["celula_centro"]),
Paragraph(nome, estilos["celula"]),
Paragraph(_moeda(empresa.honorario), estilos["celula_direita"]),
Paragraph(_moeda(empresa.honorario_ajustado), estilos["celula_direita"]),
Paragraph(TIPO_LABEL_CURTO.get(empresa.tipo, empresa.tipo), estilos["celula_centro"]),
Paragraph(_moeda(empresa.valor_individual), estilos["celula_direita"]),
Paragraph(_moeda(empresa.valor_grupo), estilos["celula_direita"]),
Paragraph(_moeda(empresa.valor_departamento), estilos["celula_direita"]),
Paragraph(_moeda(empresa.valor_total), estilos["celula_direita"]),
]
)
dados.append(
[
"",
Paragraph("Total do Indicador", estilos["celula_negrito"]),
"",
"",
"",
Paragraph(_moeda(sum((empresa.valor_individual for empresa in empresas), Decimal("0"))), estilos["celula_direita_negrito"]),
Paragraph(_moeda(sum((empresa.valor_grupo for empresa in empresas), Decimal("0"))), estilos["celula_direita_negrito"]),
Paragraph(_moeda(sum((empresa.valor_departamento for empresa in empresas), Decimal("0"))), estilos["celula_direita_negrito"]),
Paragraph(_moeda(colaborador.valor_total), estilos["celula_direita_negrito"]),
]
)
tabela = Table(
dados,
repeatRows=1,
colWidths=[1.0 * cm, 3.3 * cm, 2.0 * cm, 2.0 * cm, 2.1 * cm, 1.7 * cm, 1.6 * cm, 1.9 * cm, 1.7 * cm],
)
estilo = [
("BACKGROUND", (0, 0), (-1, 0), COR_ROXO_ESCURO),
("FONTSIZE", (0, 0), (-1, -1), 7.6),
("GRID", (0, 0), (-1, -1), 0.5, COR_BORDA),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("TOPPADDING", (0, 0), (-1, -1), 4),
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
# Padding lateral padrão do reportlab é 6pt de cada lado — generoso
# demais pra colunas numéricas estreitas; reduzido pra sobrar mais
# espaço de verdade pro texto sem precisar alargar a tabela inteira.
("LEFTPADDING", (0, 0), (-1, -1), 3),
("RIGHTPADDING", (0, 0), (-1, -1), 3),
("ROWBACKGROUNDS", (0, 1), (-1, -2), [colors.white, COR_ROXO_CLARO]),
("BACKGROUND", (0, -1), (-1, -1), COR_DOURADO_CLARO),
("LINEABOVE", (0, -1), (-1, -1), 1, COR_DOURADO),
]
tabela.setStyle(TableStyle(estilo))
return tabela
def _tabela_indicadores(colaborador: IndicadorApuracaoColaborador, apuracao: IndicadorApuracao, estilos: dict) -> Table:
periodo_atual = apuracao.periodo_criterio()
grupos = _agrupa_por_periodo(_respostas_aplicaveis(colaborador, periodo_atual))
dados = [["Atingido?", "Critério", "Peso", "% Medido"]]
estilo = [
("FONTSIZE", (0, 0), (-1, -1), 8),
("GRID", (0, 0), (-1, -1), 0.5, COR_BORDA),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("TOPPADDING", (0, 0), (-1, -1), 4),
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
("ALIGN", (0, 0), (0, -1), "CENTER"),
("ALIGN", (2, 0), (3, -1), "CENTER"),
("BACKGROUND", (0, 0), (-1, 0), COR_ROXO_ESCURO),
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
]
linha = 1
for periodo, respostas in grupos:
dados.append([PERIODO_LABEL.get(periodo, periodo), "", "", ""])
estilo.append(("SPAN", (0, linha), (3, linha)))
estilo.append(("BACKGROUND", (0, linha), (-1, linha), COR_DOURADO_CLARO))
estilo.append(("FONTNAME", (0, linha), (-1, linha), "Helvetica-Bold"))
estilo.append(("TEXTCOLOR", (0, linha), (-1, linha), COR_ROXO_ESCURO))
estilo.append(("ALIGN", (0, linha), (-1, linha), "LEFT"))
linha += 1
for resposta in respostas:
dados.append(
[
RESPOSTA_LABEL.get(resposta.valor, resposta.valor),
Paragraph(resposta.criterio.nome, estilos["celula"]),
_percentual(resposta.criterio.peso),
_percentual(resposta.percentual_calculado) if resposta.percentual_calculado is not None else "—",
]
)
cor_bg = COR_ATINGIDO_BG.get(resposta.valor, COR_NEUTRO_BG)
cor_texto = COR_ATINGIDO_TEXTO.get(resposta.valor, COR_NEUTRO_TEXTO)
estilo.append(("BACKGROUND", (0, linha), (0, linha), cor_bg))
estilo.append(("TEXTCOLOR", (0, linha), (0, linha), cor_texto))
estilo.append(("FONTNAME", (0, linha), (0, linha), "Helvetica-Bold"))
linha += 1
tabela = Table(dados, colWidths=[2 * cm, 11.4 * cm, 1.8 * cm, 2 * cm])
tabela.setStyle(TableStyle(estilo))
return tabela
def _tabela_percentuais_por_tipo(apuracao: IndicadorApuracao, departamento_id: int | None) -> Table:
dados = [["Cód.", "Tipo", "% Individual", "% Grupo", "% Depto."]]
estilo = [
("FONTSIZE", (0, 0), (-1, -1), 8),
("GRID", (0, 0), (-1, -1), 0.5, COR_BORDA),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("TOPPADDING", (0, 0), (-1, -1), 3),
("BOTTOMPADDING", (0, 0), (-1, -1), 3),
("ALIGN", (0, 0), (0, -1), "CENTER"),
("ALIGN", (2, 0), (-1, -1), "CENTER"),
("BACKGROUND", (0, 0), (-1, 0), COR_ROXO_ESCURO),
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, COR_ROXO_CLARO]),
]
for tipo, label in TIPO_COLABORADOR_INDICADOR_CHOICES:
percentual = percentual_vigente(tipo, apuracao.competencia, departamento_id)
if percentual is None:
continue
dados.append(
[
tipo,
label,
_percentual(percentual.percentual_individual),
_percentual(percentual.percentual_grupo),
_percentual(percentual.percentual_departamento),
]
)
tabela = Table(dados, colWidths=[1.5 * cm, 5.5 * cm, 3 * cm, 3 * cm, 3 * cm])
tabela.setStyle(TableStyle(estilo))
return tabela
def _rodape(colaborador: IndicadorApuracaoColaborador, apuracao: IndicadorApuracao, estilos: dict) -> list:
ultimo_dia = calendar.monthrange(apuracao.competencia.year, apuracao.competencia.month)[1]
data_extenso = f"{CIDADE_ESCRITORIO}, {ultimo_dia} de {MES_LABEL[apuracao.competencia.month]} de {apuracao.competencia.year}."
return [
Spacer(1, 1 * cm),
Paragraph(data_extenso, estilos["rodape"]),
Spacer(1, 1.4 * cm),
Paragraph("DE ACORDO,", estilos["rodape"]),
Spacer(1, 1.3 * cm),
Paragraph("_" * 45, estilos["rodape"]),
Paragraph(f"<b>{colaborador.nome}</b>", estilos["rodape"]),
]
def gera_pdf_recibo(colaborador: IndicadorApuracaoColaborador, apuracao: IndicadorApuracao) -> bytes:
buffer = io.BytesIO()
documento = SimpleDocTemplate(
buffer, pagesize=A4, topMargin=1.3 * cm, bottomMargin=1.3 * cm, leftMargin=1.5 * cm, rightMargin=1.5 * cm
)
estilos = _estilos()
elementos = [
_cabecalho(apuracao, estilos),
Spacer(1, 0.35 * cm),
Paragraph(
f"<b>Nome:</b> {colaborador.nome} &nbsp;&nbsp;·&nbsp;&nbsp; <b>Gerente:</b> {colaborador.gerente or '-'} "
f"&nbsp;&nbsp;·&nbsp;&nbsp; <b>Departamento:</b> "
f"{colaborador.departamento.nome if colaborador.departamento_id else '-'}",
estilos["normal"],
),
Spacer(1, 0.3 * cm),
_banner_percentual(colaborador, estilos),
Spacer(1, 0.5 * cm),
Paragraph("Empresas", estilos["secao"]),
_tabela_empresas(colaborador, estilos),
Spacer(1, 0.3 * cm),
Paragraph(f"<b>Total do Indicador de Desempenho: {_moeda(colaborador.valor_total)}</b>", estilos["normal"]),
Paragraph("Critérios do Indicador", estilos["secao"]),
_tabela_indicadores(colaborador, apuracao, estilos),
Paragraph("Percentuais por Tipo de Colaborador", estilos["secao"]),
_tabela_percentuais_por_tipo(apuracao, colaborador.departamento_id),
]
elementos.extend(_rodape(colaborador, apuracao, estilos))
documento.build(elementos)
return buffer.getvalue()

View File

@ -0,0 +1,110 @@
"""Derivação do "tipo" de cada colaborador por empresa (Contábil+Fiscal/
Contador SC/Contador CC/Fiscal/Conciliador) — regra confirmada com o usuário,
a partir de quem fez o Balancete/Liberação Fiscal/Conciliação Financeira de
cada empresa na planilha Serviços Tareffa. Ver "Derivação do tipo por
empresa" no plano desta feature."""
from __future__ import annotations
import datetime as dt
from dataclasses import dataclass
from .leiaute import (
SERVICO_CONCILIACAO_FINANCEIRA,
SERVICO_LIBERACAO_FISCAL,
SERVICOS_BALANCETE,
STATUS_CANCELADO,
LinhaTareffa,
)
TIPO_CONTABIL_FISCAL = "1"
TIPO_CONTADOR_SC = "2"
TIPO_CONTADOR_CC = "3"
TIPO_FISCAL = "4"
TIPO_CONCILIADOR = "5"
@dataclass
class PapelPorEmpresa:
codigo_empresa: str
nome_empresa: str
responsavel: str
tipo: str
@dataclass
class ResultadoDerivacaoTipos:
papeis: list[PapelPorEmpresa]
avisos: list[str]
def _mais_recente(linhas: list[LinhaTareffa]) -> LinhaTareffa | None:
"""Entre várias execuções do mesmo papel/empresa no mês (retrabalho), usa
a de `data_entrega` mais recente — linhas ainda sem `data_entrega` perdem
para qualquer uma já entregue."""
if not linhas:
return None
return max(linhas, key=lambda linha: linha.data_entrega or dt.date.min)
def deriva_tipos_por_empresa(linhas: list[LinhaTareffa]) -> ResultadoDerivacaoTipos:
linhas_validas = [linha for linha in linhas if linha.status != STATUS_CANCELADO]
por_empresa: dict[str, list[LinhaTareffa]] = {}
for linha in linhas_validas:
por_empresa.setdefault(linha.codigo_empresa, []).append(linha)
papeis: list[PapelPorEmpresa] = []
avisos: list[str] = []
for codigo_empresa, linhas_empresa in por_empresa.items():
nome_empresa = linhas_empresa[0].razao_social
balancete = _mais_recente([linha for linha in linhas_empresa if linha.servico in SERVICOS_BALANCETE])
fiscal = _mais_recente([linha for linha in linhas_empresa if linha.servico == SERVICO_LIBERACAO_FISCAL])
conciliacao = _mais_recente(
[linha for linha in linhas_empresa if linha.servico == SERVICO_CONCILIACAO_FINANCEIRA]
)
responsavel_balancete = balancete.responsavel if balancete else None
responsavel_fiscal = fiscal.responsavel if fiscal else None
responsavel_conciliacao = conciliacao.responsavel if conciliacao else None
responsaveis = {r for r in (responsavel_balancete, responsavel_fiscal, responsavel_conciliacao) if r}
for responsavel in responsaveis:
eh_balancete = responsavel == responsavel_balancete
eh_fiscal = responsavel == responsavel_fiscal
eh_conciliacao = responsavel == responsavel_conciliacao
if eh_balancete and eh_fiscal:
tipo = TIPO_CONTABIL_FISCAL
elif eh_balancete:
sem_conciliador_dedicado = responsavel_conciliacao is None or responsavel_conciliacao == responsavel
tipo = TIPO_CONTADOR_SC if sem_conciliador_dedicado else TIPO_CONTADOR_CC
elif eh_fiscal and eh_conciliacao:
# Regra do usuário não cobre esse caso (faz fiscal e conciliação,
# mas não o balancete, na mesma empresa) — prioriza Fiscal e avisa
# pra revisão manual, ver "Pontos a validar" do plano.
avisos.append(
f"{responsavel}: faz Liberação Fiscal e Conciliação Financeira na empresa "
f"{nome_empresa} ({codigo_empresa}) sem fazer o Balancete — considerado Fiscal "
"por padrão, confirme manualmente na revisão."
)
tipo = TIPO_FISCAL
elif eh_fiscal:
tipo = TIPO_FISCAL
elif eh_conciliacao:
tipo = TIPO_CONCILIADOR
else:
continue
papeis.append(
PapelPorEmpresa(
codigo_empresa=codigo_empresa,
nome_empresa=nome_empresa,
responsavel=responsavel,
tipo=tipo,
)
)
return ResultadoDerivacaoTipos(papeis=papeis, avisos=avisos)

View File

View File

@ -0,0 +1,130 @@
"""Registra o primeiro histórico de critérios e percentuais por tipo do
Indicador de Desempenho (Fiscontábil) — os mesmos valores da planilha antiga
("CRITÉRIO DE CALCULO DAS METAS"/"CRITÉRIOS DO CÁLCULO"), confirmados com o
usuário. Idempotente: pode ser rodado de novo sem duplicar (`update_or_create`
por nome para critérios, `get_or_create` por tipo+vigência para percentuais —
percentuais nunca são editados in-place, ver IndicadorPercentualTipo).
"""
import datetime
from decimal import Decimal
from typing import Any
from django.core.management.base import BaseCommand
from portal_api.models import IndicadorCriterio, IndicadorDepartamento, IndicadorPercentualTipo
# Todo este histórico é do Fisco/Contábil (escopo v1 da ferramenta, ver
# CLAUDE.md) — criado/reaproveitado aqui em vez de assumir que já existe,
# já que `IndicadorCriterio`/`IndicadorPercentualTipo` agora exigem um
# `IndicadorDepartamento` (rodada "Departamento organizacional").
NOME_DEPARTAMENTO_PADRAO = "Fisco/Contábil"
# "Vigente desde" do primeiro registro — não há uma data de início documentada
# na planilha antiga, então usamos o começo de 2024 como marco anterior a
# qualquer competência que venha a ser apurada nesta ferramenta. Se a política
# de fato começou a valer numa data diferente, ajustar aqui antes de rodar (ou
# cadastrar um novo registro com a data certa pela própria tela depois).
PERCENTUAL_VIGENTE_DESDE = datetime.date(2024, 1, 1)
PERCENTUAIS_POR_TIPO: list[dict[str, Any]] = [
{"tipo": "1", "percentual_individual": Decimal("1.4214"), "percentual_grupo": Decimal("20"), "percentual_departamento": Decimal("20")},
{"tipo": "2", "percentual_individual": Decimal("1.2214"), "percentual_grupo": Decimal("20"), "percentual_departamento": Decimal("20")},
{"tipo": "3", "percentual_individual": Decimal("1.0286"), "percentual_grupo": Decimal("20"), "percentual_departamento": Decimal("20")},
{"tipo": "4", "percentual_individual": Decimal("1.1786"), "percentual_grupo": Decimal("20"), "percentual_departamento": Decimal("20")},
{"tipo": "5", "percentual_individual": Decimal("0.3571"), "percentual_grupo": Decimal("20"), "percentual_departamento": Decimal("20")},
]
CRITERIOS: list[dict[str, Any]] = [
{
"nome": "Entrega de 95% das Guias de Impostos conforme os prazos estipulados no Tareffa",
"grupo": IndicadorCriterio.GRUPO_DEPARTAMENTO,
"peso": Decimal("30"),
"periodo": IndicadorCriterio.PERIODO_TODOS,
},
{
"nome": "Limite de 20 não-conformidades do fisco contábil",
"grupo": IndicadorCriterio.GRUPO_GRUPO,
"peso": Decimal("10"),
"periodo": IndicadorCriterio.PERIODO_TODOS,
},
{
"nome": "Envio de 90% dos balancetes no prazo (Março a Novembro)",
"grupo": IndicadorCriterio.GRUPO_INDIVIDUAL,
"peso": Decimal("60"),
"periodo": IndicadorCriterio.PERIODO_MAR_A_NOV,
"calculo_automatico": IndicadorCriterio.CALCULO_BALANCETE,
"limiar_percentual": Decimal("90"),
},
{
"nome": "Execução de 90% das conciliações no prazo (Março a Novembro)",
"grupo": IndicadorCriterio.GRUPO_INDIVIDUAL,
"peso": Decimal("60"),
"periodo": IndicadorCriterio.PERIODO_MAR_A_NOV,
"calculo_automatico": IndicadorCriterio.CALCULO_CONCILIACAO,
"limiar_percentual": Decimal("90"),
},
{
"nome": "Execução de 90% das liberações fiscais no prazo (Março a Novembro)",
"grupo": IndicadorCriterio.GRUPO_INDIVIDUAL,
"peso": Decimal("60"),
"periodo": IndicadorCriterio.PERIODO_MAR_A_NOV,
"calculo_automatico": IndicadorCriterio.CALCULO_LIBERACAO_FISCAL,
"limiar_percentual": Decimal("90"),
},
{
"nome": "Envio de 50% dos balancetes mensais no prazo (Dezembro a Janeiro)",
"grupo": IndicadorCriterio.GRUPO_INDIVIDUAL,
"peso": Decimal("35"),
"periodo": IndicadorCriterio.PERIODO_DEZ_A_JAN,
"calculo_automatico": IndicadorCriterio.CALCULO_BALANCETE,
"limiar_percentual": Decimal("50"),
},
{
"nome": "Entrega das Guias de Impostos em até 3 dias úteis antes do vencimento (Dezembro a Janeiro)",
"grupo": IndicadorCriterio.GRUPO_DEPARTAMENTO,
"peso": Decimal("30"),
"periodo": IndicadorCriterio.PERIODO_DEZ_A_JAN,
},
]
class Command(BaseCommand):
help = "Registra o primeiro histórico de critérios e percentuais do Indicador de Desempenho (Fiscontábil)."
def handle(self, *args: Any, **options: Any) -> None:
departamento, _ = IndicadorDepartamento.objects.get_or_create(nome=NOME_DEPARTAMENTO_PADRAO)
for dados in CRITERIOS:
criterio, criado = IndicadorCriterio.objects.update_or_create(
nome=dados["nome"],
departamento=departamento,
defaults={
"grupo": dados["grupo"],
"peso": dados["peso"],
"periodo": dados["periodo"],
"papel_aplicavel": dados.get("papel_aplicavel", ""),
"calculo_automatico": dados.get("calculo_automatico", ""),
"limiar_percentual": dados.get("limiar_percentual", Decimal("90")),
"ativo": True,
},
)
self.stdout.write(f"{'Criado' if criado else 'Atualizado'}: critério {criterio.nome!r}")
for dados in PERCENTUAIS_POR_TIPO:
percentual, criado = IndicadorPercentualTipo.objects.get_or_create(
tipo=dados["tipo"],
departamento=departamento,
vigente_desde=PERCENTUAL_VIGENTE_DESDE,
defaults={
"percentual_individual": dados["percentual_individual"],
"percentual_grupo": dados["percentual_grupo"],
"percentual_departamento": dados["percentual_departamento"],
},
)
self.stdout.write(
f"{'Criado' if criado else 'Já existia'}: percentual tipo {percentual.tipo} "
f"vigente desde {percentual.vigente_desde}"
)
self.stdout.write(self.style.SUCCESS("Histórico inicial do Indicador de Desempenho registrado."))

View File

@ -0,0 +1,187 @@
from typing import Any
from django.core.management.base import BaseCommand
from django.db import transaction
from portal_api import catalogo
from portal_api.models import CategoriaEvento, FuncaoTelefonia, PerfilAcesso, Usuario
# Comandos padrão da central telefônica (subtela "Funções de Telefonia" em
# Ramais) — documentação genérica, não dado específico da empresa, por isso
# populada aqui em vez de deixar a tabela vazia.
FUNCOES_TELEFONIA_SEED = [
{
"comando": "*0",
"funcao": "Resgata Ligação Transferida através da transferência assistida",
"resumo": "Transferir uma ligação",
},
{"comando": "*01 + Código de Agente", "funcao": "LogOn", "resumo": "Funções de Agente"},
{"comando": "*02", "funcao": "LogOff", "resumo": "Funções de Agente"},
{"comando": "*03", "funcao": "Entrar em Pausa", "resumo": "Funções de Agente"},
{
"comando": "*1 + Ramal",
"funcao": "Transferência direta. Ligação é transferida sem falar com o receptor",
"resumo": "Transferir uma ligação",
},
{
"comando": "*2 + Ramal ou Flash",
"funcao": "Transferência assistida: transferidor fala com receptor antes de transferir a ligação",
"resumo": "Transferir uma ligação",
},
{
"comando": "*20 + Ramal de Destino",
"funcao": "Ativa transferência automática de ligação para um ramal",
"resumo": "Siga-me",
},
{"comando": "*21", "funcao": "Desativar transferência automática", "resumo": "Siga-me"},
{"comando": "*22", "funcao": "Bloquear ramal para que não receba ligações", "resumo": "Não Perturbe"},
{"comando": "*23", "funcao": "Desbloquear ramal", "resumo": "Não Perturbe"},
{
"comando": "*5 + Ramal",
"funcao": "Puxar a ligação de um ramal conhecido",
"resumo": "Capturar ligação de outro ramal",
},
{"comando": "*503", "funcao": "Identificar um determinado Ramal", "resumo": "Descobrir o número de um Ramal"},
{
"comando": "*8",
"funcao": "Puxar a ligação de um ramal cadastrado no mesmo grupo de ramais",
"resumo": "Capturar ligação de outro ramal",
},
]
# Categorias de evento genéricas do Calendário Individual — ponto de partida,
# quem tiver "calendario-individual-criar-evento" cadastra novas pela própria
# tela (ver catalogo.py); reexecutar o seed é seguro (update_or_create por nome).
CATEGORIAS_EVENTO_SEED = [
{"nome": "Reunião", "cor": "#3b82f6"},
{"nome": "Treinamento", "cor": "#fb7185"},
{"nome": "Auditoria", "cor": "#1d4ed8"},
{"nome": "Data Comemorativa", "cor": "#2dd4bf"},
{"nome": "Outro", "cor": "#94a3b8"},
]
# Mesmas 8 personas do documento original de arquitetura do portal — antes
# viviam em pidSeedProfiles() (assets/js/profiles.js); "chaves": None marca o
# perfil de acesso total (Integração e Inovação).
PERFIS_SEED = [
{
"codigo": 1,
"nome": "Diretoria",
"gerencia_permissoes": False,
"chaves": [*catalogo.BASE_KEYS, *catalogo.SECTORAL_KEYS, "relatorios-gerenciais", "administracao"],
},
{
"codigo": 2,
"nome": "Departamento Pessoal",
"gerencia_permissoes": False,
"chaves": [*catalogo.BASE_KEYS, *catalogo.SECTORAL_KEYS],
},
{
"codigo": 3,
"nome": "Gerencial",
"gerencia_permissoes": False,
"chaves": [*catalogo.BASE_KEYS, "relatorios-gerenciais"],
},
{
"codigo": 4,
"nome": "Legalização",
"gerencia_permissoes": False,
"chaves": [*catalogo.BASE_KEYS, *catalogo.SECTORAL_KEYS],
},
{
"codigo": 5,
"nome": "Fisco/Contábil",
"gerencia_permissoes": False,
"chaves": [*catalogo.BASE_KEYS, *catalogo.SECTORAL_KEYS],
},
{
"codigo": 6,
"nome": "Financeiro",
"gerencia_permissoes": False,
"chaves": [*catalogo.BASE_KEYS, *catalogo.SECTORAL_KEYS],
},
{
"codigo": 7,
"nome": "Protocolo",
"gerencia_permissoes": False,
"chaves": [*catalogo.BASE_KEYS, *catalogo.SECTORAL_KEYS],
},
{
"codigo": 8,
"nome": "Integração e Inovação",
"gerencia_permissoes": True,
"chaves": None, # None = todos os módulos/aplicações habilitados
},
]
class Command(BaseCommand):
help = "Recria os 8 perfis de acesso padrão e os usuários de demonstração (gabriel/bruno)."
@transaction.atomic
def handle(self, *args: Any, **options: Any) -> None:
for dado in PERFIS_SEED:
permissoes = (
catalogo.empty_permissions(all_enabled=True)
if dado["chaves"] is None
else catalogo.permissions_from_keys(dado["chaves"])
)
if dado["codigo"] != 8:
# "links-ferramentas" e "ramais" estão em BASE_KEYS (visualizar liberado
# pra todo perfil), mas permissions_from_keys() habilita todos os apps do
# módulo de uma vez — sem isso, os "editar" sairiam True pra todo mundo
# também. Só Integração e Inovação (código 8) deve nascer com editar=True.
# Em "links-ferramentas", as 2 aplicações da seção (cartões e Acessos
# Gerais) têm cada uma seu próprio "editar". Em "ramais", só as 3 subtelas
# com conteúdo administrável têm um "editar" (Responsável no Tareffa/Férias
# só têm "visualizar", nada a sobrescrever).
permissoes["links-ferramentas"]["apps"]["links-ferramentas-editar"] = False
permissoes["links-ferramentas"]["apps"]["acessos-gerais-editar"] = False
permissoes["ramais"]["apps"]["ramais-editar"] = False
permissoes["ramais"]["apps"]["telefones-externos-editar"] = False
permissoes["ramais"]["apps"]["funcoes-telefonia-editar"] = False
# "calendario-individual" está em BASE_KEYS (visualizar liberado pra
# todo perfil) — sem este override, todo perfil nasceria podendo criar
# evento de departamento/todos e cadastrar categoria de evento.
permissoes["calendario-individual"]["apps"]["calendario-individual-criar-evento"] = False
perfil, criado = PerfilAcesso.objects.update_or_create(
codigo=dado["codigo"],
defaults={
"nome": dado["nome"],
"ativo": True,
"gerencia_permissoes": dado["gerencia_permissoes"],
"permissoes": permissoes,
},
)
acao = "criado" if criado else "atualizado"
self.stdout.write(f"Perfil {acao}: {perfil.nome} (código {perfil.codigo})")
integracao = PerfilAcesso.objects.get(codigo=8)
# Só inicializa nome/senha/perfis na primeira criação — reexecuções não devem
# reverter alterações feitas depois pela própria tela de Usuários (senha trocada,
# perfis ajustados, cadastro completado etc.).
gabriel, gabriel_criado = Usuario.objects.get_or_create(username="gabriel", defaults={"nome": "Gabriel"})
if gabriel_criado:
gabriel.set_password("gabriel")
gabriel.save()
gabriel.perfis.set([integracao])
bruno, bruno_criado = Usuario.objects.get_or_create(username="bruno", defaults={"nome": "Bruno"})
if bruno_criado:
bruno.set_password("bruno")
bruno.save()
for dado in FUNCOES_TELEFONIA_SEED:
FuncaoTelefonia.objects.update_or_create(
comando=dado["comando"],
defaults={"funcao": dado["funcao"], "resumo": dado["resumo"]},
)
for dado in CATEGORIAS_EVENTO_SEED:
CategoriaEvento.objects.update_or_create(
nome=dado["nome"],
defaults={"cor": dado["cor"]},
)
self.stdout.write(self.style.SUCCESS("Seed concluído: 8 perfis de acesso + usuários gabriel/bruno."))

View File

@ -0,0 +1,110 @@
# Generated by Django 6.0.7 on 2026-08-03 18:39
import django.contrib.auth.models
import django.contrib.auth.validators
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='PerfilAcesso',
fields=[
('codigo', models.AutoField(primary_key=True, serialize=False)),
('nome', models.CharField(max_length=100, unique=True, verbose_name='Nome do perfil')),
('ativo', models.BooleanField(default=True, verbose_name='Ativo')),
('gerencia_permissoes', models.BooleanField(default=False, verbose_name='Gerencia permissões')),
('permissoes', models.JSONField(blank=True, default=dict, verbose_name='Permissões')),
('criado_em', models.DateTimeField(auto_now_add=True)),
('atualizado_em', models.DateTimeField(auto_now=True)),
],
options={
'verbose_name': 'Perfil de acesso',
'verbose_name_plural': 'Perfis de acesso',
'ordering': ['codigo'],
},
),
migrations.CreateModel(
name='Usuario',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('nome', models.CharField(blank=True, max_length=150, verbose_name='Nome')),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
('perfis', models.ManyToManyField(blank=True, related_name='usuarios', to='portal_api.perfilacesso', verbose_name='Perfis de acesso')),
],
options={
'verbose_name': 'user',
'verbose_name_plural': 'users',
'abstract': False,
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
migrations.CreateModel(
name='CompromissoAgenda',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('titulo', models.CharField(max_length=200, verbose_name='Título')),
('data', models.DateField(verbose_name='Data')),
('horario', models.TimeField(blank=True, null=True, verbose_name='Horário')),
('compartilhado_com_perfil', models.BooleanField(default=False, verbose_name='Compartilhado com o perfil')),
('criado_em', models.DateTimeField(auto_now_add=True)),
('dono', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='compromissos', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Compromisso da agenda',
'verbose_name_plural': 'Compromissos da agenda',
'ordering': ['data', 'horario'],
},
),
migrations.CreateModel(
name='Favorito',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('app_id', models.CharField(max_length=150, verbose_name='ID da aplicação')),
('criado_em', models.DateTimeField(auto_now_add=True)),
('usuario', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favoritos', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Favorito',
'verbose_name_plural': 'Favoritos',
'constraints': [models.UniqueConstraint(fields=('usuario', 'app_id'), name='favorito_unico_por_usuario')],
},
),
migrations.CreateModel(
name='WidgetUsuario',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('tipo', models.CharField(max_length=50, verbose_name='Tipo de widget')),
('criado_em', models.DateTimeField(auto_now_add=True)),
('usuario', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='widgets', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Widget do usuário',
'verbose_name_plural': 'Widgets do usuário',
'constraints': [models.UniqueConstraint(fields=('usuario', 'tipo'), name='widget_unico_por_usuario')],
},
),
]

View File

@ -0,0 +1,29 @@
# Generated by Django 6.0.7 on 2026-08-04 11:22
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('portal_api', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='NotificacaoDispensada',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('notif_id', models.CharField(max_length=150, verbose_name='ID da notificação')),
('criado_em', models.DateTimeField(auto_now_add=True)),
('usuario', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notificacoes_dispensadas', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Notificação dispensada',
'verbose_name_plural': 'Notificações dispensadas',
'constraints': [models.UniqueConstraint(fields=('usuario', 'notif_id'), name='notificacao_dispensada_unica_por_usuario')],
},
),
]

View File

@ -0,0 +1,34 @@
# Generated by Django 6.0.7 on 2026-08-04 11:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('portal_api', '0002_notificacaodispensada'),
]
operations = [
migrations.CreateModel(
name='LinkFerramenta',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('nome', models.CharField(max_length=100, verbose_name='Nome')),
('url', models.URLField(verbose_name='URL')),
('icone', models.ImageField(blank=True, null=True, upload_to='links_ferramentas/', verbose_name='Ícone')),
('ordem', models.PositiveIntegerField(default=0, verbose_name='Ordem')),
('criado_em', models.DateTimeField(auto_now_add=True)),
],
options={
'verbose_name': 'Link/Ferramenta',
'verbose_name_plural': 'Links/Ferramentas',
'ordering': ['ordem', 'id'],
},
),
migrations.AddField(
model_name='perfilacesso',
name='gerencia_links_ferramentas',
field=models.BooleanField(default=False, help_text='Pode reordenar, incluir e remover os cartões da tela Links & Ferramentas.', verbose_name='Gerencia Links & Ferramentas'),
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 6.0.7 on 2026-08-04 12:16
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('portal_api', '0003_linkferramenta_and_more'),
]
operations = [
migrations.RemoveField(
model_name='perfilacesso',
name='gerencia_links_ferramentas',
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 6.0.7 on 2026-08-04 13:15
import portal_api.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('portal_api', '0004_remove_perfilacesso_gerencia_links_ferramentas'),
]
operations = [
migrations.AlterField(
model_name='linkferramenta',
name='icone',
field=models.ImageField(blank=True, null=True, upload_to='links_ferramentas/', validators=[portal_api.models.validar_tamanho_icone_link], verbose_name='Ícone'),
),
]

View File

@ -0,0 +1,43 @@
# Generated by Django 6.0.7 on 2026-08-04 14:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('portal_api', '0005_alter_linkferramenta_icone'),
]
operations = [
migrations.AddField(
model_name='usuario',
name='codigo_contabit',
field=models.CharField(blank=True, max_length=50, verbose_name='Código do Contabit'),
),
migrations.AddField(
model_name='usuario',
name='codigo_folha',
field=models.CharField(blank=True, max_length=50, verbose_name='Código da Folha'),
),
migrations.AddField(
model_name='usuario',
name='codigo_questor',
field=models.CharField(blank=True, max_length=50, verbose_name='Código do Questor'),
),
migrations.AddField(
model_name='usuario',
name='codigo_tareffa',
field=models.CharField(blank=True, max_length=50, verbose_name='Código do Tareffa'),
),
migrations.AddField(
model_name='usuario',
name='data_aniversario',
field=models.DateField(blank=True, null=True, verbose_name='Data de aniversário'),
),
migrations.AddField(
model_name='usuario',
name='departamento',
field=models.CharField(blank=True, max_length=100, verbose_name='Departamento'),
),
]

View File

@ -0,0 +1,35 @@
# Generated by Django 6.0.7 on 2026-08-04 14:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('portal_api', '0006_usuario_codigo_contabit_usuario_codigo_folha_and_more'),
]
operations = [
migrations.CreateModel(
name='Departamento',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('nome', models.CharField(max_length=100, unique=True, verbose_name='Nome')),
('criado_em', models.DateTimeField(auto_now_add=True)),
],
options={
'verbose_name': 'Departamento',
'verbose_name_plural': 'Departamentos',
'ordering': ['nome'],
},
),
migrations.RemoveField(
model_name='usuario',
name='departamento',
),
migrations.AddField(
model_name='usuario',
name='departamentos',
field=models.ManyToManyField(blank=True, related_name='usuarios', to='portal_api.departamento', verbose_name='Departamentos'),
),
]

View File

@ -0,0 +1,29 @@
# Generated by Django 6.0.7 on 2026-08-04 14:47
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('portal_api', '0007_departamento_remove_usuario_departamento_and_more'),
]
operations = [
migrations.CreateModel(
name='LinkFerramentaFavorito',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('criado_em', models.DateTimeField(auto_now_add=True)),
('link', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favoritos', to='portal_api.linkferramenta')),
('usuario', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='links_favoritos', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Favorito de Link/Ferramenta',
'verbose_name_plural': 'Favoritos de Links/Ferramentas',
'constraints': [models.UniqueConstraint(fields=('usuario', 'link'), name='link_favorito_unico_por_usuario')],
},
),
]

View File

@ -0,0 +1,32 @@
# Generated by Django 6.0.7 on 2026-08-04 16:30
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('portal_api', '0008_linkferramentafavorito'),
]
operations = [
migrations.AlterModelOptions(
name='widgetusuario',
options={'ordering': ['ordem', 'id'], 'verbose_name': 'Widget do usuário', 'verbose_name_plural': 'Widgets do usuário'},
),
migrations.AddField(
model_name='widgetusuario',
name='altura',
field=models.PositiveIntegerField(blank=True, null=True, verbose_name='Altura (px)'),
),
migrations.AddField(
model_name='widgetusuario',
name='largura',
field=models.PositiveIntegerField(blank=True, null=True, verbose_name='Largura (px)'),
),
migrations.AddField(
model_name='widgetusuario',
name='ordem',
field=models.PositiveIntegerField(default=0, verbose_name='Ordem'),
),
]

View File

@ -0,0 +1,22 @@
# Generated by Django 6.0.7 on 2026-08-04 16:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('portal_api', '0009_alter_widgetusuario_options_widgetusuario_altura_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='favorito',
options={'ordering': ['ordem', 'id'], 'verbose_name': 'Favorito', 'verbose_name_plural': 'Favoritos'},
),
migrations.AddField(
model_name='favorito',
name='ordem',
field=models.PositiveIntegerField(default=0, verbose_name='Ordem'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 6.0.7 on 2026-08-04 17:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('portal_api', '0010_alter_favorito_options_favorito_ordem'),
]
operations = [
migrations.AddField(
model_name='usuario',
name='ramal',
field=models.CharField(blank=True, max_length=20, verbose_name='Ramal'),
),
]

View File

@ -0,0 +1,50 @@
# Generated by Django 6.0.7 on 2026-08-04 17:36
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('portal_api', '0011_usuario_ramal'),
]
operations = [
migrations.CreateModel(
name='Ramal',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('nome', models.CharField(blank=True, max_length=150, verbose_name='Nome')),
('departamento', models.CharField(blank=True, max_length=150, verbose_name='Departamento')),
('numero', models.CharField(blank=True, max_length=20, verbose_name='Ramal')),
('criado_em', models.DateTimeField(auto_now_add=True)),
('usuario', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='ramal_entries', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Ramal',
'verbose_name_plural': 'Ramais',
},
),
migrations.CreateModel(
name='RamalAusencia',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('data_inicio', models.DateField(verbose_name='Data inicial')),
('hora_inicio', models.TimeField(blank=True, null=True, verbose_name='Hora de início')),
('data_fim', models.DateField(verbose_name='Data final')),
('hora_volta', models.TimeField(blank=True, null=True, verbose_name='Hora da volta')),
('tipo', models.CharField(blank=True, default='Ausência', max_length=100, verbose_name='Tipo da ausência')),
('observacoes', models.TextField(blank=True, verbose_name='Observações')),
('encerrada_manualmente', models.BooleanField(default=False, verbose_name='Encerrada manualmente')),
('criado_em', models.DateTimeField(auto_now_add=True)),
('usuario', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ausencias_ramal', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Ausência de ramal',
'verbose_name_plural': 'Ausências de ramal',
'ordering': ['-data_inicio', '-id'],
},
),
]

View File

@ -0,0 +1,22 @@
# Generated by Django 6.0.7 on 2026-08-04 18:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('portal_api', '0012_ramal_ramalausencia'),
]
operations = [
migrations.RemoveField(
model_name='ramal',
name='usuario',
),
migrations.AlterField(
model_name='ramal',
name='nome',
field=models.CharField(max_length=150, verbose_name='Nome'),
),
]

View File

@ -0,0 +1,44 @@
# Generated by Django 6.0.7 on 2026-08-04 20:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('portal_api', '0013_remove_ramal_usuario_alter_ramal_nome'),
]
operations = [
migrations.CreateModel(
name='FuncaoTelefonia',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('comando', models.CharField(max_length=50, verbose_name='Comando')),
('funcao', models.TextField(verbose_name='Função')),
('resumo', models.CharField(blank=True, max_length=150, verbose_name='Resumo')),
('criado_em', models.DateTimeField(auto_now_add=True)),
],
options={
'verbose_name': 'Função de telefonia',
'verbose_name_plural': 'Funções de telefonia',
'ordering': ['comando'],
},
),
migrations.CreateModel(
name='TelefoneExterno',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('nome', models.CharField(max_length=150, verbose_name='Nome')),
('ramal', models.CharField(blank=True, max_length=20, verbose_name='Ramal')),
('telefone', models.CharField(blank=True, max_length=30, verbose_name='Telefone')),
('observacoes', models.TextField(blank=True, verbose_name='Observações')),
('criado_em', models.DateTimeField(auto_now_add=True)),
],
options={
'verbose_name': 'Telefone externo',
'verbose_name_plural': 'Telefones externos',
'ordering': ['nome'],
},
),
]

View File

@ -0,0 +1,24 @@
# Generated by Django 6.0.7 on 2026-08-05 13:32
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('portal_api', '0014_funcaotelefonia_telefoneexterno'),
]
operations = [
migrations.AddField(
model_name='usuario',
name='liderados',
field=models.ManyToManyField(blank=True, related_name='lideres', to=settings.AUTH_USER_MODEL, verbose_name='Liderados'),
),
migrations.AddField(
model_name='usuario',
name='lideranca',
field=models.BooleanField(default=False, verbose_name='É gerente ou coordenador'),
),
]

View File

@ -0,0 +1,47 @@
# Generated by Django 6.0.7 on 2026-08-05 17:07
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('portal_api', '0015_usuario_liderados_usuario_lideranca'),
]
operations = [
migrations.CreateModel(
name='AcessoGeralSecao',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('nome', models.CharField(max_length=150, verbose_name='Nome')),
('ordem', models.PositiveIntegerField(default=0, verbose_name='Ordem')),
('criado_em', models.DateTimeField(auto_now_add=True)),
],
options={
'verbose_name': 'Seção de Acessos Gerais',
'verbose_name_plural': 'Seções de Acessos Gerais',
'ordering': ['ordem', 'id'],
},
),
migrations.CreateModel(
name='AcessoGeral',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('nome', models.CharField(max_length=150, verbose_name='Nome')),
('url', models.URLField(blank=True, verbose_name='URL')),
('usuario', models.CharField(blank=True, max_length=150, verbose_name='Usuário')),
('senha', models.CharField(blank=True, max_length=255, verbose_name='Senha')),
('observacoes', models.TextField(blank=True, verbose_name='Observações')),
('ordem', models.PositiveIntegerField(default=0, verbose_name='Ordem')),
('criado_em', models.DateTimeField(auto_now_add=True)),
('secao', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='acessos', to='portal_api.acessogeralsecao')),
],
options={
'verbose_name': 'Acesso Geral',
'verbose_name_plural': 'Acessos Gerais',
'ordering': ['ordem', 'id'],
},
),
]

Some files were not shown because too many files have changed in this diff Show More