diff --git a/docs/calendario-individual/CHANGELOG.md b/docs/calendario-individual/CHANGELOG.md index 919d8f8..36455ea 100644 --- a/docs/calendario-individual/CHANGELOG.md +++ b/docs/calendario-individual/CHANGELOG.md @@ -24,3 +24,13 @@ Compromissos ganharam campo de horário (ordenados por horário dentro do dia) e 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 ``, que só devolvia valor vazio, e a tela respondia "Informe até quando o razão foi exportado", sem apontar a data inválida. O ideal pedido: corrigir sozinho para 30/09/2026. O campo nativo não expõe o que foi digitado quando a data é inválida, então virou texto dd/mm/aaaa com máscara (`pidConcCampoData()` em `conciliacao-fornecedores.js`, nos dois campos: Nova Conciliação e Atualizar razão). Dia além do fim do mês vira o último dia do mês, com aviso abaixo do campo (inclui 29/02 em ano não bissexto). Mês inválido, dia 00 ou data incompleta mostram erro específico. O calendário continua disponível pelo botão do campo, que abre o seletor nativo de um `` escondido; esse campo escondido mantém o id antigo e o valor ISO, então o resto da tela e a API não mudaram. + +### Rodada 54 — "Razão até" em branco na Nova Conciliação (2026-09-30) + +Pedido do usuário: o campo vinha preenchido com o último dia do mês anterior (escolha da rodada 50 desta pasta, não pedida pelo usuário); agora a Nova Conciliação abre com ele em branco, e o contador informa a data. O modal Atualizar razão continua sugerindo a data (a mesma da Nova Conciliação, quando vem de lá; senão o último dia do mês anterior, nunca antes do fim atual). + +### Rodada 55 — Ajuste de data sem aviso (2026-09-30) + +Pedido do usuário: o texto "31/09/2026 não existe: ajustado para o último dia do mês..." é desnecessário. A correção para o último dia do mês continua, agora sem mensagem; o aviso abaixo do campo aparece só para data inválida (dia 00, mês fora de 1 a 12) ou incompleta. + +### Rodada 56 — Melhorias da revisão de interface de 30/09/2026 (2026-09-30) + +Pedido do usuário: implementar o relatório `docs/revisao-interface-2026-09-30-conciliacao-fornecedores.docx` (versão sem acessibilidade, decisão do usuário). Feito: + +- **Observação não se perde**: o texto digitado e não salvo (aba da conciliação e aba dos títulos antigos) fica em `rascunhos`/`rascunhosVenc` por conta e sobrevive ao redesenho da conta (vincular, validar pelo checkbox), ao recolher/expandir e à troca de filtro. Sair da conciliação com rascunho pendente ("Voltar ao histórico", Voltar do navegador, fechar ou recarregar a página) pede confirmação. "Atualizar razão" reabre a mesma conciliação mantendo os rascunhos. +- **Sem envio duplo**: "Salvar observação" e "Marcar como validada/Remover validação" (nas duas abas) ficam desabilitados até a resposta. +- **Erro no formulário leva o cursor ao campo** (código da empresa, "Razão até"), na Nova Conciliação e no Atualizar razão. +- **Estado na URL**: aba, filtro de situação/alerta, "Mostrar validadas", ordenação e filtro/ordem da aba de títulos antigos vão para o hash (`#conciliacao-?...`); recarregar volta ao mesmo ponto. Abrir uma conciliação pela lista agora empilha uma entrada (`pushState`): o Voltar do navegador volta à lista em vez de sair da tela. +- **Busca** (histórico e as duas abas do detalhe) espera 150 ms sem digitação antes de redesenhar. +- **Sem `title` nativo**: checkbox de validado, "marcar todos" e "Adicionar selecionados" usam o tooltip da tela. +- **Acabamento**: reticências "…" nos textos de espera, botões e placeholders; `tabular-nums` em `.conc-num`; `touch-action: manipulation` nas linhas clicáveis; `name` nos campos; `cursor: pointer` que sobrou no input de data escondido removido. + +Não feito: `content-visibility: auto` nas linhas de fornecedor (sugerido no relatório). A propriedade não tem efeito em linhas de tabela (a contenção não se aplica a ``), e aplicá-la nas células desalinharia as colunas; o ganho real exigiria virtualizar a tabela, desproporcional para ~260 linhas. + +### Rodada 57 — Relatório de análise: logo embutida e ano com 4 dígitos (2026-10-01) + +Itens da revisão de interface de 30/09/2026 no relatório de análise para o contador (`templates/conciliacao-fornecedores-relatorio.html`): + +- A logo vem embutida como data URI (`logo_data_uri`, montado por `_logo_relatorio_data_uri()` em `views.py`, o mesmo do Relatório Contábil), com fallback para o `{% static %}`: o HTML salvo e aberto fora do Portal não perde mais a imagem. Mexe em `views.py`: exige reiniciar o `runserver`. +- As datas de "validada em", "última ação" e "revisado em" passam de `d/m/y H:i` para `d/m/Y H:i`, como as demais datas do relatório. diff --git a/portal_api/conciliacao_fornecedores/CLAUDE.md b/portal_api/conciliacao_fornecedores/CLAUDE.md index 29e3631..12d3a3b 100644 --- a/portal_api/conciliacao_fornecedores/CLAUDE.md +++ b/portal_api/conciliacao_fornecedores/CLAUDE.md @@ -21,7 +21,7 @@ O contador exporta do Questor o razão contábil das contas de fornecedores (que ## Leitura do razão (`parser.py`) -Cada conta começa numa linha "Conta: - " (coluna Histórico), seguida de "Saldo anterior..." (só a coluna Saldo) e dos lançamentos. Não há linha de total nem cabeçalho de empresa/período: o **início** do período é a data do "Saldo anterior" + 1 dia (não o primeiro lançamento, que pode vir depois), e o **fim** é informado pelo contador no campo "Razão até" da criação e da atualização (o arquivo só traz até o último lançamento, e um mês sem movimento encurtaria o período; rodada 50 do CHANGELOG). `_conciliacao_aplica_periodo_fim()` (views.py) recusa data antes do último lançamento, antes do início, futura (serializer) ou, na atualização, antes do fim atual. Valores com lado "8.214,92D"/"16.429,84C". **Convenção interna: credor positivo** (valor devido ao fornecedor). `saldo_final_calculado` (saldo anterior + créditos − débitos) é comparado com o último saldo impresso (`saldo_final_arquivo`); diferença vira alerta alto (exportação incompleta). No arquivo de referência (`Projetos\Conciliação\Teste Exportação - Fornecedores - 2017.xlsx`, empresa 2017, 260 contas, 2.520 lançamentos, jan-ago/2026) as 260 contas batem. +Cada conta começa numa linha "Conta: - " (coluna Histórico), seguida de "Saldo anterior..." (só a coluna Saldo) e dos lançamentos. Não há linha de total nem cabeçalho de empresa/período: o **início** do período é a data do "Saldo anterior" + 1 dia (não o primeiro lançamento, que pode vir depois), e o **fim** é informado pelo contador no campo "Razão até" da criação e da atualização (o arquivo só traz até o último lançamento, e um mês sem movimento encurtaria o período; rodada 50 do CHANGELOG). `_conciliacao_aplica_periodo_fim()` (views.py) recusa data antes do último lançamento, antes do início, futura (serializer) ou, na atualização, antes do fim atual. Na tela, o campo é texto dd/mm/aaaa (`pidConcCampoData()`, com botão que abre o calendário nativo de um `` escondido que guarda o ISO): dia além do fim do mês é ajustado para o último dia (31/09 → 30/09, em silêncio: o aviso abaixo do campo é só para data inválida ou incompleta, rodada 55), porque o campo nativo só devolvia valor vazio sem dizer o motivo (rodada 53 do CHANGELOG). Valores com lado "8.214,92D"/"16.429,84C". **Convenção interna: credor positivo** (valor devido ao fornecedor). `saldo_final_calculado` (saldo anterior + créditos − débitos) é comparado com o último saldo impresso (`saldo_final_arquivo`); diferença vira alerta alto (exportação incompleta). No arquivo de referência (`Projetos\Conciliação\Teste Exportação - Fornecedores - 2017.xlsx`, empresa 2017, 260 contas, 2.520 lançamentos, jan-ago/2026) as 260 contas batem. Cada coluna é achada pelo início do nome no cabeçalho, só letras sem acento (ex. "SE" para "Seqüência", que pode vir com o acento corrompido; "DESC" para Descrição, para não confundir com Débito). Obrigatórias: Data, Seqüência, Histórico, Contrapartida, Descrição, Valor, Saldo; opcionais: Filial, Usuário, Origem; Débito, Crédito e Participante são ignoradas. @@ -97,7 +97,7 @@ Todas com `PermissaoApp("utilitarios", "conciliacao-fornecedores")`. | Endpoint | Método | Uso | |---|---|---| | `/api/conciliacoes-fornecedores/` | GET/POST | histórico (`?codigo_empresa=` filtra) / criação multipart (só `codigo_empresa` + `arquivo` + `periodo_fim` ("Razão até"); data-base e tolerâncias são definidas no servidor) | -| `/api/conciliacoes-fornecedores/{id}/` | GET/DELETE | detalhe completo (contas + lançamentos + vínculos + análise, ~1 MB para 2.500 lançamentos; reaplica a validação automática e regrava o `resumo` se mudou) / exclusão (apaga o arquivo depois do commit) | +| `/api/conciliacoes-fornecedores/{id}/` | GET | detalhe completo (contas + lançamentos + vínculos + análise, ~1 MB para 2.500 lançamentos; reaplica a validação automática e regrava o `resumo` se mudou). **Sem DELETE** (405), decisão explícita do usuário por rastreabilidade: com vários usuários na ferramenta, excluir apagaria o trabalho sem registro de quem excluiu. Exclusão só via código (shell/script) ou quando o usuário pedir explicitamente; não reintroduzir o botão nem o método sem confirmar | | `/api/conciliacoes-fornecedores/nome-empresa/?codigo_empresa=` | GET | nome no Questor (`null` se não achou ou Questor fora) | | `/api/conciliacoes-fornecedores/{id}/exportar-xlsx/` | GET | planilha com abas Resumo, Pendências e Alertas | | `/api/conciliacoes-fornecedores/{id}/atualizar/` | POST | multipart `arquivo` + `periodo_fim` ("Razão até", não antes do fim atual): razão novo que substitui o da conciliação (ver "Atualização do razão"); devolve `{conciliacao, atualizacao}` (contagens) | @@ -113,7 +113,7 @@ Os três endpoints de ajuste devolvem `{conta, resumo}`: a conta reanalisada e o ## Tela (`conciliacao-fornecedores.html` / `.js` / `.css`) -Duas vistas na mesma página. **Histórico**: formulário de nova conciliação (o código da empresa busca o nome no Questor com debounce; o nome do arquivo com prefixo numérico sugere o código) + tabela do histórico. **Detalhe** (`#conciliacao-` na URL), desenhado para ser leve (pedido do usuário, validado antes num protótipo): +Duas vistas na mesma página. **Histórico**: formulário de nova conciliação (o código da empresa busca o nome no Questor com debounce; o nome do arquivo com prefixo numérico sugere o código) + tabela do histórico. **Detalhe** (`#conciliacao-` na URL, seguido de aba, filtros e ordenação: `#conciliacao-22?aba=vencidos&status=divergente&ordem=-saldo_final`, lido por `pidConcLeHash()`/`aplicarParametros()` e regravado com `replaceState` a cada redesenho por `atualizarUrl()`; abrir pela lista empilha uma entrada com `pushState`, então o Voltar do navegador e o "Voltar ao histórico" voltam à lista pelo `popstate`; rodada 56 do CHANGELOG), desenhado para ser leve (pedido do usuário, validado antes num protótipo). **Observação digitada e não salva** (das duas abas) fica em `rascunhos`/`rascunhosVenc` (por conta), sobrevive ao redesenho, ao recolher e à troca de filtro, e sair da conciliação (Voltar, Voltar do navegador, fechar/recarregar a página) com rascunho pendente pergunta antes; "Salvar observação"/"Marcar como validada" ficam travados durante o envio. Buscas com espera de 150 ms (`pidConcAtrasado()`). Nenhum `title` nativo na tela: as dicas usam `[data-conc-tooltip]` (no "Adicionar selecionados" desabilitado, no invólucro `[data-adicionar-dica]`, porque botão desabilitado não recebe o mouse): - cabeçalho da empresa baixo, código e nome na mesma linha; - **uma faixa de indicadores**: saldo em aberto, em aberto há mais de 3 meses (clica e abre a aba própria, com quantos fornecedores já foram validados nela), pagamentos sem título (divergência, clica e filtra) e uma **barra de progresso da validação** ("146 de 260"); - os apontamentos menores (diferença de juros/desconto, pagamento sem título de mesmo valor, conta transitória) como **etiquetas** clicáveis numa linha, só quando existem; diff --git a/portal_api/custo_contratacao/CHANGELOG.md b/portal_api/custo_contratacao/CHANGELOG.md index acb6cb5..37a52c4 100644 --- a/portal_api/custo_contratacao/CHANGELOG.md +++ b/portal_api/custo_contratacao/CHANGELOG.md @@ -9,3 +9,17 @@ 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 técnico completo no `CLAUDE.md` desta pasta. + +### Rodada 38 — Revisão de interface (30/09/2026): validação, tabelas fiscais e formulário no celular + +Correções de interface apontadas pela revisão de interface de 30/09/2026, só no frontend (`custo-contratacao.js`/`.css`/`.html`); nada mudou no cálculo nem no backend. + +- **Tabelas fiscais sem valor inválido gravado como 0**: `parseValorBrEstrito()` aceita só número no formato brasileiro ("1.234,56" ou "1234,56"); campo inválido fica marcado em vermelho (`.is-invalido`), o cursor vai ao primeiro deles e nada é salvo. Antes um valor inválido virava 0 sem aviso e era gravado para todos os usuários. +- **Tabelas fiscais com milhar e coeficiente com vírgula**: os inputs das faixas e dos parâmetros passam a ser preenchidos com separador de milhar ("7.350,00", `formataValorBrMilhar`), compatível com `parseValorBrEstrito` ao salvar. O coeficiente B da Lei 15.270/2025 aparece com vírgula ("0,133145"); o salvar continua aceitando vírgula ou ponto. `formataValorBr` (sem milhar) continua sendo a usada no payload da simulação. +- **Falha ao carregar as tabelas fiscais**: mensagem com próximo passo ("Recarregue a página; se persistir, contate a Inovação.") e "Salvar tabelas" desabilitado até a carga terminar com sucesso, para não gravar tabelas vazias. +- **Validação do formulário**: item de "Outros itens"/"Outros benefícios"/"Outros descontos" com valor e sem nome bloqueia o envio (antes era descartado do cálculo sem aviso) e leva o cursor ao nome; salário contratual, % de INSS Empregador e % de FGTS em branco também levam o cursor ao campo, em vez de só mostrar a mensagem no rodapé. +- **Gerar PDF**: a aba é aberta no clique, antes do `fetch` (senão o bloqueador de pop-up costuma barrar), recebe o PDF quando ele chega e o blob é revogado depois; os botões mostram "Gerando…"/"Salvando…" e ficam travados até terminar. +- **Limpar**: pede confirmação (`pidConfirm`, perigoso) só quando há algo preenchido no formulário ou alguma linha incluída nos itens. +- **Campos e layout**: campos monetários e percentuais com `inputmode="decimal"` (inclusive as faixas criadas pelo JS); placeholder do coeficiente B "Ex.: 0,133145…"; linhas de 2 e 3 colunas do formulário e do painel fiscal viram uma coluna em tela de celular (até 640px, com escopo só no conteúdo da Simulação, sem afetar os modais comuns do shell); `tabular-nums` nos inputs das faixas. + +Ficaram sem `inputmode="decimal"` de propósito os campos que aceitam valor **ou** percentual ("Adicional Art. 62" e os três descontos, ex.: "40%"): em teclado virtual de celular esse modo pode não oferecer o "%". diff --git a/portal_api/dashboard_contabil/CHANGELOG.md b/portal_api/dashboard_contabil/CHANGELOG.md index 6d6fe89..41167ea 100644 --- a/portal_api/dashboard_contabil/CHANGELOG.md +++ b/portal_api/dashboard_contabil/CHANGELOG.md @@ -506,3 +506,33 @@ Pedido do usuário, com print: numa análise que só tem apontamentos de variaç - Validar uma variação na Análise Vertical agora redesenha também a aba Observações, para as contagens de pendentes acompanharem. Só `.html`/`.css`/`.js`: não exige reiniciar o `runserver`. O JS não pôde ser executado neste ambiente: conferir na tela. + +### Rodada 155 — Correções da revisão de interface de 30/09/2026 + +Pedido do usuário: corrigir os problemas de interface apontados na revisão de interface de 30/09/2026 (versão sem acessibilidade para teclado e leitor de tela, decisão do usuário). + +**Feito antes, no mesmo dia** (inclui backend): +- `dashboard-contabil.html`: botões "Gerar Dashboard" passam a "Gerar Relatório". +- Relatório do cliente: as observações mostram o código da conta (filtro novo `codigo_conta` em `templatetags/contabil_extras.py`, aplicado a `obs.alvo_chave`) em vez da chave interna `codigo|descricao`; o `colspan` da Análise Vertical usa o contexto novo `analise_vertical_colunas`; a logo vem embutida como data URI (`logo_data_uri`, montado por `_logo_relatorio_data_uri()` em `views.py`), com fallback para o `{% static %}`, para o HTML salvo e aberto fora do Portal não perder a imagem; aberto por `file:`, o `` ganha `dcr-fora-do-portal`, que esconde os botões de exportar (dependem de login no Portal). + +**Tela de revisão** (`dashboard-contabil.js`/`.html`/`.css`): +- **Falha não fica silenciosa**: abrir uma análise, abrir a aba Dashboard (busca dos indicadores), "Concluir Análise" e a carga inicial da lista têm tratamento de erro. A carga inicial com erro mostra a mensagem de falha no lugar da lista, não "Nenhuma análise realizada ainda". `abrirRevisao()` só troca `apuracaoAtual` depois das duas buscas darem certo. Recarregar a lista ou os indicadores depois que um modal já fechou vira aviso (`pidAlert`), não erro escrito num modal invisível. +- **Sem envio duplo**: "Marcar como tratado"/"Ignorar" (os três botões do modal travados juntos), "Concluir Análise", "Abrir", salvar/excluir indicador, Processar, Reprocessar, salvar o Resumo do Fechamento e salvar observação (nova e edição) ficam desabilitados com "Salvando…"/"Processando…"/"Excluindo…"/"Abrindo…" até a resposta. +- **Modal "Revisar"**: fechar pelo overlay ou Cancelar com a justificativa alterada pergunta "Sair sem salvar as alterações?"; durante o envio não fecha. +- **Modal de indicador**: o "Sair sem salvar" só aparece quando algo mudou (foto do formulário inteiro, com componentes e seleções, tirada ao abrir). +- **Modal "Reprocessar"**: não fecha pelo overlay durante o processamento, então o erro continua visível no modal. +- **Texto não salvo não se perde**: o campo "Escreva uma observação nova…" e a edição inline guardam cada tecla por painel (`dcObsRascunhos`, chave `nova:-`/`edit:`) e são repostos no fim de cada render, então expandir/recolher grupo, trocar os chips Todas/Visíveis/Internas, usar o olho, encerrar, excluir ou validar uma conta (antes aceito como perda na documentação) não esvaziam mais o campo. Fechar um painel com texto (Fechar, o próprio ícone, abrir outro painel no mesmo escopo, ir para a variação) pergunta antes. Voltar, Concluir, Reprocessar a apuração aberta e abrir outra análise perguntam quando há observação em digitação ou Resumo do Fechamento alterado (comparado ao valor logo depois de carregado ou salvo, não ao retorno sanitizado pelo `nh3`), e fechar/recarregar a página dispara o aviso do navegador (`beforeunload`). Mesmo modelo da Conciliação de Fornecedores. +- **Tooltip da tela no lugar do `title` nativo**: todos os botões com `title` (HTML e JS) passaram a `data-dc-dica`, mostrado por um balão único com o visual de `.dc-hover-tooltip__texto` (`pidDcMostraDica()`, `.dc-dica-flutuante`), sem envolver o botão em outro elemento. O balão some ao clicar (a tabela costuma ser redesenhada) e ao rolar. +- **Toque**: em `@media (hover: none)`, tocar no badge de "mudou no reprocessamento" (com o valor anterior) ou no de fonte de PDF atípica abre o tooltip (`.is-aberto`, `pidDcAlternaTooltipToque()`); tocar de novo ou fora fecha. +- **Barra de progresso** anima `transform: scaleX()` com `transform-origin: left`, não `width`. +- **Descrição da DRE/Análise Vertical** quebra linha (`white-space: normal` com `min-width: 16rem`) em vez de ser cortada sem reticências pela borda da tabela. +- **Acabamento**: reticências "…" nos placeholders, textos de espera e no corte do tooltip de variação; `name` e `autocomplete="off"` nos três campos de texto do modal de indicador. + +**Relatório do cliente** (`dashboard-contabil-relatorio.html`): +- O hover do card de indicador (subir 3px) nunca aparecia porque o `animation ... both` mantinha o `transform` do último quadro: o card usa `animation-fill-mode: backwards`, que guarda o estado inicial no atraso e solta o estilo ao terminar. A impressão não muda (o `@media print` já zera toda `animation`). +- Indicador deslizante das abas anima só `transform` (`translateX` + `scaleX` sobre largura base de 100px), não `width`. +- Verso do card abre também no toque (`.is-virado`, só em `@media (hover: none)`, para o clique do mouse não deixar o card preso no verso); na impressão sai sempre a frente. + +**Não feito**: moeda negativa no relatório sai "R$ -1.234,50" e na tela "-R$ 1.234,50"; a diferença está no filtro `moeda` de `contabil_extras.py` (backend), fora do escopo desta rodada. Estado na URL, otimização de `dcDescendentes()` e as fontes do Google no relatório ficam para outra rodada. + +Só `.html`/`.css`/`.js` nesta parte: não exige reiniciar o `runserver` (as mudanças de backend do mesmo dia, sim). O JS passou no `node --check`, mas não foi executado num navegador neste ambiente: conferir na tela. diff --git a/portal_api/indicadores/CHANGELOG.md b/portal_api/indicadores/CHANGELOG.md index 897f943..ba12c44 100644 --- a/portal_api/indicadores/CHANGELOG.md +++ b/portal_api/indicadores/CHANGELOG.md @@ -125,3 +125,23 @@ Pedido, testando a rodada 45: o campo "Adicionar gerente" era só texto livre 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 ` @@ -1569,14 +1786,20 @@ document.addEventListener("DOMContentLoaded", async () => { `; const observacaoEl = validacao.querySelector("[data-observacao]"); + observacaoEl.addEventListener("input", () => rascunhos.set(conta.id, observacaoEl.value)); + const botoesValidacao = validacao.querySelectorAll("[data-salvar-obs], [data-validar]"); async function salvarValidacao(validada) { + // travados até a resposta: um clique duplo mandaria duas vezes + botoesValidacao.forEach((btn) => { btn.disabled = true; }); try { const resposta = await pidApiRequest(`/conciliacoes-fornecedores-contas/${conta.id}/validar/`, { method: "POST", body: { validada, observacao: observacaoEl.value }, }); + rascunhos.delete(conta.id); aplicarRespostaConta(resposta); } catch (erro) { + botoesValidacao.forEach((btn) => { btn.disabled = false; }); pidAlert(erro.message, { titulo: "Não foi possível salvar" }); } } @@ -1691,7 +1914,7 @@ document.addEventListener("DOMContentLoaded", async () => { renderVencidos(); }); }); - vencBusca.addEventListener("input", renderVencidos); + vencBusca.addEventListener("input", pidConcAtrasado(() => detalhe && renderVencidos())); function atualizarContagemVencidos() { const todas = contasVencidas(); @@ -1727,6 +1950,7 @@ document.addEventListener("DOMContentLoaded", async () => { const fragmento = document.createDocumentFragment(); lista.forEach((conta) => montarLinhasVencidos(conta).forEach((tr) => fragmento.appendChild(tr))); vencBody.appendChild(fragmento); + atualizarUrl(); } function montarLinhasVencidos(conta) { @@ -1799,7 +2023,7 @@ document.addEventListener("DOMContentLoaded", async () => {
- +
@@ -1811,10 +2035,14 @@ document.addEventListener("DOMContentLoaded", async () => {
`; const observacaoEl = painel.querySelector("[data-venc-observacao]"); + observacaoEl.addEventListener("input", () => rascunhosVenc.set(conta.id, observacaoEl.value)); + const botoesValidacao = painel.querySelectorAll("[data-venc-salvar-obs], [data-venc-validar]"); const salvar = async (validada) => { + botoesValidacao.forEach((btn) => { btn.disabled = true; }); try { await salvarVencidos(conta, validada, observacaoEl.value); } catch (erro) { + botoesValidacao.forEach((btn) => { btn.disabled = false; }); pidAlert(erro.message, { titulo: "Não foi possível salvar" }); } }; @@ -1858,6 +2086,7 @@ document.addEventListener("DOMContentLoaded", async () => { method: "POST", body: corpo, }); + if (observacao !== undefined) rascunhosVenc.delete(conta.id); const indice = detalhe.contas.findIndex((c) => c.id === resposta.conta.id); const eraValidada = indice >= 0 && detalhe.contas[indice].vencidos_validados; if (indice >= 0) detalhe.contas[indice] = resposta.conta; @@ -1872,9 +2101,9 @@ document.addEventListener("DOMContentLoaded", async () => { // ====================================================================== // Início // ====================================================================== - const hash = window.location.hash.match(/^#conciliacao-(\d+)$/); + const hash = pidConcLeHash(); if (hash) { - abrirDetalhe(Number(hash[1])); + abrirDetalhe(hash.id, { params: hash.params }); } else { carregarHistorico(); } diff --git a/static/js/custo-contratacao.js b/static/js/custo-contratacao.js index 681c811..8a41695 100644 --- a/static/js/custo-contratacao.js +++ b/static/js/custo-contratacao.js @@ -26,10 +26,38 @@ document.addEventListener("DOMContentLoaded", async () => { return isNaN(numero) ? 0 : numero; } + /** Número no formato brasileiro ("1.234,56", "7,5", "1234") ou `null` se + * o texto não for um número. Usado nas tabelas fiscais, onde um valor + * inválido virava 0 sem aviso e era gravado para todos (revisão de + * 30/09/2026). */ + function parseValorBrEstrito(texto) { + const limpo = String(texto || "").trim(); + if (!/^-?(\d{1,3}(\.\d{3})+|\d+)(,\d+)?$/.test(limpo)) return null; + return parseFloat(limpo.replace(/\./g, "").replace(",", ".")); + } + + // Sem separador de milhar: usada pra montar o payload (percentual + // convertido em valor), que o servidor lê como número simples. function formataValorBr(numero) { return numero.toFixed(2).replace(".", ","); } + // Com separador de milhar ("7.350,00"), só pra preencher os inputs das + // tabelas fiscais: parseValorBrEstrito aceita as duas formas + // ("1.234,56" e "1234,56") ao salvar. + const PID_CC_VALOR_EXIBICAO = new Intl.NumberFormat("pt-BR", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); + function formataValorBrMilhar(numero) { + return PID_CC_VALOR_EXIBICAO.format(Number(numero) || 0); + } + + // Coeficiente B da Lei 15.270/2025 tem muitas casas e vem do banco com + // ponto ("0.133145"): exibido com vírgula, sem arredondar e sem milhar. + // O salvar aceita vírgula ou ponto. + function formataCoeficienteBr(numero) { + if (numero === null || numero === undefined || numero === "") return ""; + return String(numero).replace(".", ","); + } + /** * Converte um texto que pode ser um valor em reais (ex.: "3.400,00") ou um * percentual do salário contratual (ex.: "40%") pro valor em reais final — @@ -132,7 +160,7 @@ document.addEventListener("DOMContentLoaded", async () => { * importacao-plano-saude.js), mas abre o resultado numa aba nova em vez de * forçar download — a ideia é já deixar o PDF pronto pra visualizar/imprimir/enviar. */ - async function geraPdfSimulacao(payload) { + async function geraPdfSimulacao(payload, janela) { await pidEnsureCsrfCookie(); const response = await fetch("/api/simulacao-custo-contratacao/gerar/", { method: "POST", @@ -156,7 +184,56 @@ document.addEventListener("DOMContentLoaded", async () => { const blob = await response.blob(); const url = URL.createObjectURL(blob); - window.open(url, "_blank"); + // A aba foi aberta no clique (antes de qualquer await), senão o + // bloqueador de pop-up costuma barrar; sem ela, tenta abrir agora. + if (janela && !janela.closed) janela.location.href = url; + else window.open(url, "_blank"); + setTimeout(() => URL.revokeObjectURL(url), 60000); + } + + /** Linha de item com valor preenchido e sem nome: antes era descartada do + * cálculo sem aviso. Devolve o nome da seção com problema, ou "". */ + function secaoComItemSemNome() { + const secoes = [ + [remuneracaoOutrosBody, "Outros itens de remuneração"], + [beneficiosOutrosBody, "Outros benefícios"], + [descontosOutrosBody, "Outros descontos"], + ]; + for (const [tbody, rotulo] of secoes) { + const semNome = Array.from(tbody.children).find( + (tr) => !tr.querySelector('[data-campo="nome"]').value.trim() && tr.querySelector('[data-campo="valor"]').value.trim() + ); + if (semNome) { + semNome.querySelector('[data-campo="nome"]').focus(); + return rotulo; + } + } + return ""; + } + + // Leva o cursor ao campo apontado por uma mensagem de validação (a + // mensagem fica no rodapé do formulário, longe do campo). + function focarCampo(id) { + const campo = document.getElementById(id); + if (!campo) return; + campo.scrollIntoView({ block: "center", behavior: "smooth" }); + campo.focus({ preventScroll: true }); + } + + // Há algo preenchido no formulário da simulação (campo com valor diferente + // do original do HTML, ou linha incluída em "Outros itens"/"Outros + // benefícios"/"Outros descontos")? Só nesse caso "Limpar" pergunta antes. + function formularioTemConteudo() { + const campoAlterado = Array.from(formEl.querySelectorAll("input, select, textarea")).some((el) => { + if (el.type === "checkbox" || el.type === "radio") return el.checked !== el.defaultChecked; + return el.value.trim() !== (el.defaultValue || "").trim(); + }); + return ( + campoAlterado || + remuneracaoOutrosBody.children.length > 0 || + beneficiosOutrosBody.children.length > 0 || + descontosOutrosBody.children.length > 0 + ); } formEl.addEventListener("submit", async (event) => { @@ -166,18 +243,27 @@ document.addEventListener("DOMContentLoaded", async () => { const payload = montaPayload(); if (!payload.salario_contratual) { errorEl.textContent = "Informe o salário contratual."; + focarCampo("cc-salario"); return; } if (!payload.percentual_inss_empregador || !payload.percentual_fgts) { errorEl.textContent = "Informe o % de INSS Empregador e o % de FGTS."; + focarCampo(payload.percentual_inss_empregador ? "cc-perc-fgts" : "cc-perc-inss"); + return; + } + const secaoSemNome = secaoComItemSemNome(); + if (secaoSemNome) { + errorEl.textContent = `Em "${secaoSemNome}", há um item com valor e sem nome. Informe o nome ou remova a linha.`; return; } + const janela = window.open("", "_blank"); submitBtn.disabled = true; - submitBtn.textContent = "Gerando..."; + submitBtn.textContent = "Gerando…"; try { - await geraPdfSimulacao(payload); + await geraPdfSimulacao(payload, janela); } catch (e) { + if (janela) janela.close(); errorEl.textContent = e.message; } finally { submitBtn.disabled = false; @@ -186,7 +272,17 @@ document.addEventListener("DOMContentLoaded", async () => { }); if (limparBtn) { - limparBtn.addEventListener("click", () => { + limparBtn.addEventListener("click", async () => { + if ( + formularioTemConteudo() && + !(await pidConfirm("Limpar todos os campos da simulação? O que foi preenchido será perdido.", { + titulo: "Limpar simulação", + textoConfirmar: "Limpar", + perigoso: true, + })) + ) { + return; + } formEl.reset(); remuneracaoOutrosBody.innerHTML = ""; beneficiosOutrosBody.innerHTML = ""; @@ -214,13 +310,13 @@ document.addEventListener("DOMContentLoaded", async () => { function criarLinhaFaixa(faixa) { const tr = document.createElement("tr"); - const limite = formataValorBr(faixa.limite_superior); - const aliquota = formataValorBr(faixa.aliquota * 100); - const deduzir = formataValorBr(faixa.deduzir); + const limite = formataValorBrMilhar(faixa.limite_superior); + const aliquota = formataValorBrMilhar(faixa.aliquota * 100); + const deduzir = formataValorBrMilhar(faixa.deduzir); tr.innerHTML = - '' + - '' + - '' + + '' + + '' + + '' + ''; tr.querySelector('[data-campo="limite_superior"]').value = limite; tr.querySelector('[data-campo="aliquota"]').value = aliquota; @@ -234,35 +330,53 @@ document.addEventListener("DOMContentLoaded", async () => { faixas.forEach((faixa) => tbodyEl.appendChild(criarLinhaFaixa(faixa))); } + // Campos inválidos da última tentativa de salvar (marcados em vermelho). + let camposInvalidos = []; + + /** Lê um campo numérico das tabelas fiscais; inválido entra na lista de + * erros (e fica marcado) em vez de virar 0. */ + function lerNumeroFiscal(input) { + const numero = parseValorBrEstrito(input.value); + input.classList.toggle("is-invalido", numero === null); + if (numero === null) camposInvalidos.push(input); + return numero === null ? 0 : numero; + } + function lerFaixasDoTbody(tbodyEl) { return Array.from(tbodyEl.children).map((tr) => { const valores = {}; tr.querySelectorAll("input[data-campo]").forEach((input) => { - valores[input.dataset.campo] = parseValorBr(input.value); + valores[input.dataset.campo] = lerNumeroFiscal(input); }); return { - limite_superior: valores.limite_superior || 0, - aliquota: (valores.aliquota || 0) / 100, - deduzir: valores.deduzir || 0, + limite_superior: valores.limite_superior, + aliquota: valores.aliquota / 100, + deduzir: valores.deduzir, }; }); } + // "Salvar tabelas" fica desabilitado até a carga terminar com sucesso: + // salvar com os campos ainda vazios (carga em andamento ou falha) gravaria + // tabelas zeradas pra todos. async function carregarParametrosFiscais() { + if (painelSalvarBtn) painelSalvarBtn.disabled = true; try { const parametros = await pidApiRequest("/parametros-fiscais-custo-contratacao/"); renderFaixas(inssBody, parametros.faixas_inss); renderFaixas(irrfBody, parametros.faixas_irrf); - document.getElementById("cc-inss-teto").value = formataValorBr(parametros.teto_desconto_inss); - document.getElementById("cc-irrf-topo-aliquota").value = formataValorBr(parametros.aliquota_irrf_topo * 100); - document.getElementById("cc-irrf-topo-deduzir").value = formataValorBr(parametros.deduzir_irrf_topo); - document.getElementById("cc-desconto-simplificado").value = formataValorBr(parametros.desconto_simplificado_irrf); - document.getElementById("cc-deducao-dependente").value = formataValorBr(parametros.deducao_por_dependente); - document.getElementById("cc-lei15270-a").value = formataValorBr(parametros.reducao_lei_15270_coeficiente_a); - document.getElementById("cc-lei15270-b").value = String(parametros.reducao_lei_15270_coeficiente_b); - document.getElementById("cc-lei15270-limite").value = formataValorBr(parametros.reducao_lei_15270_limite); + document.getElementById("cc-inss-teto").value = formataValorBrMilhar(parametros.teto_desconto_inss); + document.getElementById("cc-irrf-topo-aliquota").value = formataValorBrMilhar(parametros.aliquota_irrf_topo * 100); + document.getElementById("cc-irrf-topo-deduzir").value = formataValorBrMilhar(parametros.deduzir_irrf_topo); + document.getElementById("cc-desconto-simplificado").value = formataValorBrMilhar(parametros.desconto_simplificado_irrf); + document.getElementById("cc-deducao-dependente").value = formataValorBrMilhar(parametros.deducao_por_dependente); + document.getElementById("cc-lei15270-a").value = formataValorBrMilhar(parametros.reducao_lei_15270_coeficiente_a); + document.getElementById("cc-lei15270-b").value = formataCoeficienteBr(parametros.reducao_lei_15270_coeficiente_b); + document.getElementById("cc-lei15270-limite").value = formataValorBrMilhar(parametros.reducao_lei_15270_limite); + if (painelSalvarBtn) painelSalvarBtn.disabled = false; } catch (e) { - painelError.textContent = "Não foi possível carregar as tabelas fiscais."; + painelError.textContent = + "Não foi possível carregar as tabelas fiscais. Recarregue a página; se persistir, contate a Inovação. O salvar fica desabilitado até as tabelas carregarem."; } } @@ -284,20 +398,36 @@ document.addEventListener("DOMContentLoaded", async () => { painelSalvarBtn.addEventListener("click", async () => { painelError.textContent = ""; painelStatus.hidden = true; + camposInvalidos = []; + const campo = (id) => lerNumeroFiscal(document.getElementById(id)); + // Coeficiente B tem muitas casas: exibido com vírgula ("0,133145", ver + // formataCoeficienteBr), aceita ponto ou vírgula decimal, sem + // separador de milhar. + const coeficienteBInput = document.getElementById("cc-lei15270-b"); + const coeficienteB = /^\d+([.,]\d+)?$/.test(coeficienteBInput.value.trim()) + ? parseFloat(coeficienteBInput.value.trim().replace(",", ".")) + : null; + coeficienteBInput.classList.toggle("is-invalido", coeficienteB === null); + if (coeficienteB === null) camposInvalidos.push(coeficienteBInput); const payload = { faixas_inss: lerFaixasDoTbody(inssBody), - teto_desconto_inss: parseValorBr(campoValor("cc-inss-teto")), + teto_desconto_inss: campo("cc-inss-teto"), faixas_irrf: lerFaixasDoTbody(irrfBody), - aliquota_irrf_topo: parseValorBr(campoValor("cc-irrf-topo-aliquota")) / 100, - deduzir_irrf_topo: parseValorBr(campoValor("cc-irrf-topo-deduzir")), - desconto_simplificado_irrf: parseValorBr(campoValor("cc-desconto-simplificado")), - deducao_por_dependente: parseValorBr(campoValor("cc-deducao-dependente")), - reducao_lei_15270_coeficiente_a: parseValorBr(campoValor("cc-lei15270-a")), - reducao_lei_15270_coeficiente_b: parseFloat(campoValor("cc-lei15270-b").replace(",", ".")) || 0, - reducao_lei_15270_limite: parseValorBr(campoValor("cc-lei15270-limite")), + aliquota_irrf_topo: campo("cc-irrf-topo-aliquota") / 100, + deduzir_irrf_topo: campo("cc-irrf-topo-deduzir"), + desconto_simplificado_irrf: campo("cc-desconto-simplificado"), + deducao_por_dependente: campo("cc-deducao-dependente"), + reducao_lei_15270_coeficiente_a: campo("cc-lei15270-a"), + reducao_lei_15270_coeficiente_b: coeficienteB || 0, + reducao_lei_15270_limite: campo("cc-lei15270-limite"), }; + if (camposInvalidos.length) { + painelError.textContent = `${camposInvalidos.length} campo(s) com valor inválido, marcados em vermelho. Use números no formato 1.234,56; nada foi salvo.`; + camposInvalidos[0].focus(); + return; + } painelSalvarBtn.disabled = true; - painelSalvarBtn.textContent = "Salvando..."; + painelSalvarBtn.textContent = "Salvando…"; try { const parametros = await pidApiRequest("/parametros-fiscais-custo-contratacao/", { method: "PATCH", diff --git a/static/js/dashboard-contabil.js b/static/js/dashboard-contabil.js index 64ebcd4..30b8635 100644 --- a/static/js/dashboard-contabil.js +++ b/static/js/dashboard-contabil.js @@ -192,12 +192,15 @@ function pidDcEscapeHtml(texto) { // `completar()` (chamado quando a resposta volta) fecha os últimos 10% e // segura em 100% por um instante, pra sempre refletir o fim de verdade do // processamento, nunca um término fake antes da hora. +// O avanço é um `transform: scaleX()` sobre uma barra de largura 100% (ver +// `.dc-progress__fill` no CSS), não `width`: só `transform` anima sem refazer +// o layout a cada passo. function pidDcIniciarProgressoSimulado(fillEl) { let pct = 0; - fillEl.style.width = "0%"; + fillEl.style.transform = "scaleX(0)"; const intervalId = setInterval(() => { pct += (90 - pct) * 0.1; - fillEl.style.width = `${pct}%`; + fillEl.style.transform = `scaleX(${pct / 100})`; }, 200); return { parar() { @@ -205,7 +208,7 @@ function pidDcIniciarProgressoSimulado(fillEl) { }, async completar() { clearInterval(intervalId); - fillEl.style.width = "100%"; + fillEl.style.transform = "scaleX(1)"; await new Promise((resolve) => setTimeout(resolve, 300)); }, }; @@ -228,7 +231,12 @@ function pidDcPosicionaTooltip(event) { if (!gatilho) return; const texto = gatilho.nextElementSibling; if (!texto || !texto.classList.contains("dc-hover-tooltip__texto")) return; + pidDcPosicionaBalao(gatilho, texto); +} +// Cálculo de posição compartilhado pelo tooltip dos badges (acima) e pela +// dica dos botões (`data-dc-dica`, abaixo). +function pidDcPosicionaBalao(gatilho, texto) { const margem = 8; const rect = gatilho.getBoundingClientRect(); const largura = texto.offsetWidth; @@ -247,6 +255,65 @@ function pidDcPosicionaTooltip(event) { document.addEventListener("mouseenter", pidDcPosicionaTooltip, true); document.addEventListener("focus", pidDcPosicionaTooltip, true); +// Em tela de toque não existe hover: tocar num badge informativo (valor +// antes do reprocessamento, fonte de PDF atípica) abre o tooltip dele, tocar +// de novo ou em qualquer outro ponto fecha. Gatilho que contém um botão (o +// ícone de variação atípica da Análise Vertical) fica de fora, porque o toque +// nele já tem ação própria (levar ao card da variação). +function pidDcAlternaTooltipToque(event) { + if (!window.matchMedia || !window.matchMedia("(hover: none)").matches) return; + const gatilho = event.target.closest && event.target.closest(".dc-hover-tooltip__gatilho"); + const tooltip = gatilho && !gatilho.querySelector("button") ? gatilho.parentElement : null; + document.querySelectorAll(".dc-hover-tooltip.is-aberto").forEach((aberto) => { + if (aberto !== tooltip) aberto.classList.remove("is-aberto"); + }); + if (!tooltip) return; + const abriu = tooltip.classList.toggle("is-aberto"); + if (abriu) pidDcPosicionaTooltip({ target: gatilho }); +} + +document.addEventListener("click", pidDcAlternaTooltipToque); + +// Dica dos botões da tela, no lugar do `title` nativo (o balão do navegador +// quebra a identidade visual do Portal, mesmo motivo de pidDcHoverTooltipHtml): +// qualquer elemento com `data-dc-dica="texto"` mostra um balão único, com o +// mesmo visual de `.dc-hover-tooltip__texto`, criado uma vez em +// `document.body`. Diferente de pidDcHoverTooltipHtml, não envolve o botão em +// outro elemento (não mexe no layout das linhas de ação) e o texto pode mudar +// depois de desenhado (ex.: dcAtualizaBotaoArvore troca a dica do botão "−"/"+"). +let pidDcDicaEl = null; + +function pidDcEscondeDica() { + if (pidDcDicaEl) pidDcDicaEl.classList.remove("is-visivel"); +} + +function pidDcMostraDica(event) { + const alvo = event.target.closest && event.target.closest("[data-dc-dica]"); + if (!alvo) return; + const texto = alvo.getAttribute("data-dc-dica"); + if (!texto) return; + if (!pidDcDicaEl) { + pidDcDicaEl = document.createElement("span"); + pidDcDicaEl.className = "dc-hover-tooltip__texto dc-dica-flutuante"; + document.body.appendChild(pidDcDicaEl); + } + pidDcDicaEl.textContent = texto; + pidDcPosicionaBalao(alvo, pidDcDicaEl); + pidDcDicaEl.classList.add("is-visivel"); +} + +document.addEventListener("mouseover", pidDcMostraDica); +document.addEventListener("mouseout", (event) => { + const alvo = event.target.closest && event.target.closest("[data-dc-dica]"); + if (!alvo) return; + if (event.relatedTarget && alvo.contains(event.relatedTarget)) return; + pidDcEscondeDica(); +}); +// O clique costuma redesenhar a tabela (o botão some sem disparar `mouseout`) +// e a rolagem tira o balão do lugar: nos dois casos, esconde. +document.addEventListener("click", pidDcEscondeDica, true); +window.addEventListener("scroll", pidDcEscondeDica, true); + async function pidFetchApuracoesContabil() { return pidApiRequest("/contabil-apuracoes/"); } @@ -625,14 +692,14 @@ document.addEventListener("DOMContentLoaded", async () => { ${ apuracao.status === "concluida" ? "" - : `` } ${ apuracao.status === "concluida" ? "" - : `` } @@ -696,11 +763,11 @@ document.addEventListener("DOMContentLoaded", async () => { const wrapper = document.createElement("span"); wrapper.className = "dc-col-filter"; wrapper.innerHTML = ` -