# Changelog — Indicador de Desempenho > Histórico específico desta aplicação, extraído de `plano.md`. > > **Formato**: uma entrada por rodada, `### Rodada N — Título`; quando a rodada não tem número registrado, `### Título` só. **A numeração de rodada não é global** — cada aplicação conta as próprias, e o mesmo número designa trabalhos diferentes em arquivos diferentes. Ao citar uma rodada, sempre nomear o arquivo. Ver o topo de `plano.md`. ### Rodada 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` desta pasta. 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 `` 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` da raiz 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 `` (`.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. ### Rodada 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 `