Criação da bateria de testes automatizados, correção da pagina de gerar romaneio e plano da criação do modulo cliente

This commit is contained in:
2026-08-28 09:40:59 -03:00
parent b20299dd6b
commit c454cb8cc7
4 changed files with 423 additions and 2 deletions

195
README.md
View File

@@ -3513,3 +3513,198 @@ Dockerfile # tira a linha de precompile mort
``` ```
</details> </details>
---
<details>
<summary><strong>🆕 Atualização 28/08/2026 — PLANO: Módulo do Cliente (aprovado, NÃO implementado)</strong></summary>
> ⚠️ **Isto é um plano de implementação, não uma feature entregue.** Foi escrito
> como ordem de serviço para ser executado em fases (inclusive por modelos
> menores), com o levantamento do código já feito. Implementar **uma fase por
> vez**, na ordem, validando cada uma antes da próxima.
## O que é o módulo
Hoje o cliente (Gade Hospitalar) manda uma **planilha** por mês/operação, que
alguém sobe no banco como tabela `gade_entregas_*` (fora do sistema). O módulo
do cliente substitui esse fluxo: uma área **`/cliente`** com login próprio onde
o próprio cliente:
1. **Cria operações** (ex.: "UBS NORTE — setembro/2026") e lança entregas nelas;
2. **Adiciona entregas em operações existentes**;
3. Lança **entregas avulsas** (fora de qualquer operação);
4. Cadastra **em massa via planilha .xlsx** (o módulo é um espelho da planilha
atual — mesmos campos), com conferência antes de gravar;
5. Recebe **detecção de duplicidade na hora**: "você tem X apontamentos
duplicados" e "a NF Y já está roteirizada / em curso".
## 🔒 Regras invioláveis (valem para TODAS as fases)
- `db_reem_simplerout_2026` e as `gade_entregas_*` são **somente leitura**:
nunca migration, nunca INSERT/UPDATE/DELETE/DROP nelas. O módulo grava apenas
em **tabelas próprias novas**.
- Todo nome de tabela externa que entra em SQL passa por `Operacao.sanitizar` +
`quote_table_name` (padrão já usado no projeto inteiro).
- **NÃO transformar `app/models/operacao.rb` em ActiveRecord.** Ele é um PORO
(só métodos de classe) que descobre as tabelas externas pelo catálogo do
Postgres e é usado por ~8 services. Os models novos têm outros nomes.
- Seguir o CLAUDE.md do projeto: número na tela é contrato; modo de visualização
em aba, não tela nova; mobile-first com alvos de 48px; comentário explica o
porquê; validar com Docker (`ruby -c` / actionview) antes de entregar.
- Ao final de cada fase: listar os `git add` por nome (commit/push são do
usuário) e lembrar que o deploy para o teste é manual.
## 📌 Fatos do código que o implementador precisa saber
| Assunto | Onde está | O que copiar |
|---|---|---|
| "Operação" hoje | `app/models/operacao.rb` (PORO) | catálogo/whitelist (`nomes_validos`, `sanitizar`, `label`, `MESES`) — só consumir |
| Colunas da planilha do cliente | `app/services/simpli_route/planilha_carga.rb` (`COLUNAS_OP`) | `nota_fiscal`, `nome_completo`, `endereco_sem_complemento`, `complemento`, `supervisao`, `telefones`; geo nas gade é `lat`/`long` |
| Esquema variável por mês | `app/services/romaneios/enriquecimento_gade.rb` | detecção de colunas via `conn.columns(tabela)`; só `nota_fiscal` é garantida |
| Dedup existente (melhor modelo) | `app/services/romaneios/importador.rb#deduplicar` + `RomaneioLinha.chave_para` + `app/services/romaneios/normalizador.rb` | chave normalizada (NF + título + endereço), primeira ocorrência vence, contagem reportada |
| Rastreio (roteirizado/em curso) | `app/models/entrega.rb` (`db_reem_simplerout_2026`, `readonly?=true`) | scopes `da_conta_gade`, `por_nf`; status da visita indica o andamento |
| Ler .xlsx | gem `roo` (~2.10, já no Gemfile; usada no romaneio) | |
| Gerar .xlsx | gem `caxlsx` (já no Gemfile; usada na planilha SimpliRoute) | |
| Papéis/permissões | `app/models/user.rb` (enum `role`), `app/models/permissao.rb`, `app/models/perfil_acesso.rb`, policies em `app/policies/` | `Permissao::TODAS` + `GRUPOS` + `PADRAO_POR_ROLE` (esquecer o PADRAO tira acesso silenciosamente — aviso no próprio model) |
| Migration de permissão (modelo) | `db/migrate/20260827000004_add_permissao_romaneio_aos_perfis.rb` | sincronizar perfis existentes |
| Rotas | `config/routes.rb` | seguir o padrão `namespace :admin`; atenção ao aviso do Inflector pt-BR no próprio arquivo (singular == plural gera `_index`) |
| Notificações (evento novo) | infra em `app/models/notificacao_*` / `app/services/notificacao/` | criar evento "cliente lançou entregas" na Fase 5 |
**Não existe hoje nenhum conceito de "cliente" no código** (nem model, nem
coluna, nem namespace) — a fundação é a Fase 0.
## Fase 0 — Fundação de acesso (`/cliente` existe e loga)
1. `User`: adicionar `cliente: 5` ao enum `role`.
2. `Permissao`: novas chaves no grupo novo `cliente`:
`cliente.ver`, `cliente.operacoes_criar`, `cliente.entregas_lancar`,
`cliente.importar_planilha`, `cliente.avulsas` — em `TODAS`, `GRUPOS` e
`PADRAO_POR_ROLE` (role `cliente` recebe todas as `cliente.*` e NADA de
dashboard/consolidação/admin).
3. `HOME_POR_PERMISSAO` / `home_rota`: usuário com `cliente.ver` cai em `/cliente`.
4. Rotas: `namespace :cliente` com `root`, `resources :operacoes` (conferir o
plural com o Inflector), `resources :entregas`, `resource :importacao`.
5. Layout `app/views/layouts/cliente.html.erb`: identidade visual do projeto
(dark, laranja `#f97316`), **sem** menus de admin; menu próprio: Operações ·
Avulsas · Importar planilha · Sair.
6. Policies Pundit novas baseadas em `user.pode?('cliente.…')`; os controllers
de `/cliente` herdam de um `Cliente::BaseController` que bloqueia quem não
tem `cliente.ver`.
7. Migration sincronizando perfis + seeds: perfil "Cliente" e um usuário de
teste `cliente@reem.com`.
**Pronto quando**: login com usuário cliente cai em `/cliente` (vazio, com estado
vazio instrutivo), e esse usuário NÃO acessa `/dashboard`, `/consolidacoes` nem
`/admin/*` (redireciona para `/sem-acesso`); admin continua acessando tudo.
## Fase 1 — Modelagem (tabelas próprias)
1. `ClienteOperacao` (`cliente_operacoes`): `nome`, `mes`, `ano`,
`status` (enum: `rascunho` → `enviada` → `roteirizada` → `fechada`),
`criado_por_id` (FK users), timestamps. Índice único em
`[nome, mes, ano]` (case-insensitive).
2. `ClienteEntrega` (`cliente_entregas`): `cliente_operacao_id` (**nulo =
avulsa**), campos espelhando a planilha atual — `nota_fiscal`,
`nome_completo`, `endereco_sem_complemento`, `complemento`, `supervisao`,
`telefones`, `lat`, `long`, `observacoes` — mais `chave_dedup` (string
normalizada), `origem` (enum: `manual`/`planilha`), `criado_por_id`,
timestamps.
3. `chave_dedup`: gerada em callback com a MESMA normalização de
`Romaneios::Normalizador` (extrair para um módulo compartilhável se preciso,
sem quebrar os chamadores atuais). Índice único parcial em
`[cliente_operacao_id, chave_dedup]` — e validação amigável antes do índice.
4. Validações: `nota_fiscal` obrigatória; `nome_completo` ou endereço presentes.
**Pronto quando**: migrations rodam no Docker, models com validações e specs de
unicidade; nada de tela ainda.
## Fase 2 — Telas `/cliente` (cadastro individual intuitivo)
1. **Lista de operações**: cards (nome, mês/ano, nº de entregas, status, botão
"Adicionar entrega"); estado vazio dizendo o que fazer ("Crie sua primeira
operação ou importe uma planilha").
2. **Criar operação**: form curto (nome + mês/ano), sem jargão técnico.
3. **Adicionar entrega**: form mobile-first (alvos 4856px), campos na ordem da
planilha, busca de endereço livre; ao salvar, o painel de duplicidade da
Fase 4 responde na mesma tela (inline, não outra página).
4. **Entregas avulsas**: mesma tela de lançamento com aba/rótulo "Avulsa
(sem operação)" — mesma unidade, rótulo explícito (regra 1 do CLAUDE.md).
5. Lista de entregas da operação com edição inline (padrão Stimulus do projeto,
ver `romaneio_controller.js`) e remoção com confirmação.
**Pronto quando**: fluxo completo criar operação → lançar 3 entregas → editar →
remover, no desktop e no celular (validar com agent-browser + `set device`).
## Fase 3 — Importação em massa (.xlsx)
1. **Modelo para download**: gerar com `caxlsx` uma planilha com o cabeçalho
exato dos campos da Fase 1 + 2 linhas de exemplo (o cliente já conhece esse
formato — é a planilha que ele manda hoje).
2. **Upload**: `roo` lendo .xlsx; mapear cabeçalhos com tolerância (acentos,
caixa, espaços — reusar normalização existente).
3. **Tela de conferência ANTES de gravar** (nada entra direto): resumo com
N válidas · N com erro (motivo por linha) · N duplicadas (painel da Fase 4);
o cliente escolhe "Importar só as válidas" ou cancelar.
4. Gravação transacional (`insert_all` ou transaction com validação); relatório
final na tela com os mesmos números da conferência.
5. Limites e mensagens: arquivo até ~5 MB / ~5.000 linhas; erro de formato tem
mensagem em português dizendo o que corrigir.
**Pronto quando**: importar a planilha-modelo preenchida com linhas repetidas de
propósito grava só as válidas e reporta as duplicadas com os números certos.
## Fase 4 — Duplicidade inteligente (`Cliente::DetectorDuplicidade`)
Serviço único usado pelo form individual (Fase 2) e pela importação (Fase 3).
Entrada: coleção de entregas candidatas (+ operação alvo). Saída: para cada
candidata, `status_duplicidade` e a evidência. **Quatro camadas, nesta ordem:**
1. **Dentro do lote**: chave normalizada repetida no próprio arquivo/form —
padrão de `Romaneios::Importador#deduplicar` (primeira vence, conta o resto).
2. **Contra o módulo**: `ClienteEntrega` já gravada com a mesma `chave_dedup`
(na mesma operação ou avulsa do mesmo mês).
3. **Contra as `gade_entregas_*` do mesmo mês/ano**: SELECT read-only (tabelas
via `Operacao.sanitizar` + `quote_table_name`; só `nota_fiscal` é garantida —
detectar colunas via `conn.columns` como no `EnriquecimentoGade`).
4. **Contra o rastreio** (`Entrega.da_conta_gade.por_nf`): se a NF já existe no
espelho, classificar pelo status da visita — **"já roteirizada"** (planejada)
ou **"em curso/entregue"** (checkout feito). Conferir os valores reais de
status no espelho antes de fixar o mapeamento.
Na tela (regra 1 do CLAUDE.md — explicar onde o número aparece):
resumo "⚠️ X apontamentos duplicados neste lançamento" + lista expandível
inline com a camada que acusou cada um ("já lançada nesta operação",
"consta na planilha de agosto", "NF 79093 já roteirizada em 27/08").
Duplicata **avisa e bloqueia por padrão**, com opção explícita "lançar mesmo
assim" (grava com flag `duplicada_confirmada` para auditoria).
**Pronto quando**: os 4 cenários têm teste (fixtures/factories) e a tela mostra
a contagem e o motivo camada por camada.
## Fase 5 — Lado admin + integração
1. **Visão admin**: aba/tela em `/admin` listando lançamentos do cliente por
operação/status, com os MESMOS números que o cliente vê (recorte único).
2. **Exportação**: baixar as entregas de uma `ClienteOperacao` no formato da
planilha de carga do SimpliRoute (referência: `SimpliRoute::PlanilhaCarga`) —
é o que fecha o ciclo: o que o cliente lança vira rota.
3. **Notificação**: evento novo na infra existente ("Cliente lançou N entregas
na operação X") para o grupo do WhatsApp da operação.
4. **Auditoria**: registrar criar/editar/excluir/importar do módulo no padrão de
auditoria existente (`/admin/auditoria_logs`).
5. Transição de status da operação (`enviada` → `roteirizada`) — manual pelo
admin nesta fase; automatizar via rastreio fica para depois.
**Pronto quando**: admin vê, exporta e é notificado; auditoria registra.
## Fase 6 — Validação e encerramento
1. Adicionar à `docs/BATERIA-DE-TESTES.md` os casos: login cliente; criar
operação; lançar entrega; importar .xlsx com duplicatas propositais e conferir
as contagens; bloqueio de acesso do cliente às áreas de admin.
2. Rodar a bateria completa (`/bateria-testes`) no ambiente de teste após o
deploy manual.
3. Atualizar este README (marcar fases entregues) e o perfil "Cliente" nos seeds.
</details>

View File

@@ -133,7 +133,12 @@ export default class extends Controller {
abrirPreview() { abrirPreview() {
if (!this.hasOverlayTarget) return if (!this.hasOverlayTarget) return
// Classe + atributo: só o atributo `hidden` não basta, porque a utility
// `.flex` do Tailwind vence o `[hidden] { display:none }` do preflight —
// foi assim que o modal ficou preso na tela do /admin/romaneios/13.
this.overlayTarget.hidden = false this.overlayTarget.hidden = false
this.overlayTarget.classList.remove("hidden")
this.overlayTarget.classList.add("flex")
// Trava o scroll do fundo: rolar a lista atrás de um modal aberto faz o // Trava o scroll do fundo: rolar a lista atrás de um modal aberto faz o
// operador perder o lugar quando fecha. // operador perder o lugar quando fecha.
document.body.style.overflow = "hidden" document.body.style.overflow = "hidden"
@@ -144,6 +149,8 @@ export default class extends Controller {
if (!this.hasOverlayTarget || this.overlayTarget.hidden) return if (!this.hasOverlayTarget || this.overlayTarget.hidden) return
this.overlayTarget.hidden = true this.overlayTarget.hidden = true
this.overlayTarget.classList.remove("flex")
this.overlayTarget.classList.add("hidden")
document.body.style.overflow = "" document.body.style.overflow = ""
} }

View File

@@ -92,10 +92,17 @@
<iframe> e não <embed>/<object>: o CSP está em enforcing com <iframe> e não <embed>/<object>: o CSP está em enforcing com
`object_src :none`, e sem `frame_src` ele cai em `default_src :self` — `object_src :none`, e sem `frame_src` ele cai em `default_src :self` —
o iframe de mesma origem passa, os outros dois não. %> o iframe de mesma origem passa, os outros dois não.
O fechado/aberto é pela CLASSE `hidden`/`flex`, não só pelo atributo
`hidden`: as utilities do Tailwind são aplicadas depois do preflight,
então `.flex` vence o `[hidden] { display:none }` — com `flex` fixo no
markup o modal nascia aberto, em branco e sem fechar, cobrindo a tela
inteira (caso real: /admin/romaneios/13). O controller troca as classes
e mantém o atributo sincronizado. %>
<div data-romaneio-target="overlay" hidden <div data-romaneio-target="overlay" hidden
data-action="click->romaneio#fecharPorFora keydown.esc@window->romaneio#fecharPreview" data-action="click->romaneio#fecharPorFora keydown.esc@window->romaneio#fecharPreview"
class="fixed inset-0 z-50 flex items-center justify-center p-3 sm:p-6 class="fixed inset-0 z-50 hidden items-center justify-center p-3 sm:p-6
bg-black/70 backdrop-blur-sm"> bg-black/70 backdrop-blur-sm">
<div class="w-full max-w-6xl h-[92vh] flex flex-col gap-3" <div class="w-full max-w-6xl h-[92vh] flex flex-col gap-3"
data-romaneio-target="caixaPreview"> data-romaneio-target="caixaPreview">

212
docs/BATERIA-DE-TESTES.md Normal file
View File

@@ -0,0 +1,212 @@
# Bateria de testes — Reem Notas (ambiente de teste)
Roteiro de regressão para ser executado por um agente (inclusive modelos menores)
com a skill `agent-browser`. Não é preciso conhecer o projeto: siga os casos na
ordem e preencha o relatório do final.
## Regras de segurança (leia antes de tudo)
1. **Somente leitura + interações seguras.** É PROIBIDO:
- clicar em qualquer botão que abra confirmação (`Tem certeza?`, `Rebuscar o
plano?` etc.) — se a confirmação abrir por engano, cancele;
- finalizar, arquivar, excluir ou registrar pagamento de consolidação;
- criar, editar ou excluir usuários, perfis, contatos, grupos ou eventos;
- qualquer envio de WhatsApp/notificação.
- Formulários só podem ser preenchidos nos casos em que o roteiro mandar
explicitamente (login e campos de busca).
2. **Ambiente**: `https://teste.reemtransportes.com.br` (nunca o domínio de
produção).
3. **Avisos do ambiente de teste** (não são falhas):
- Não há pagamentos registrados (`pago_em` vazio) — gráficos e tabelas de
pagamento vazios são o esperado;
- O deploy é manual: o site pode estar atrás do código do repositório. Se um
caso falhar por algo que parece "código novo que não chegou lá", registre
como `DEPLOY?` em vez de `FALHOU`.
4. Screenshots de evidência vão para o diretório de scratchpad da sessão.
## Credenciais
- **Admin (web)**: e-mail `admin@reem.com`, senha `Reem@2026!` — em
`https://teste.reemtransportes.com.br/auth/login`, aba "E-mail".
- **Motorista**: aba "PIN Motorista" da mesma tela (PIN de 4 dígitos; se nenhum
PIN de teste for fornecido na tarefa, marque o caso 8 como `PULADO`).
## Teste 0 — transversal (rodar em TODA página visitada)
Depois de abrir cada página do roteiro, sempre rode:
```bash
agent-browser errors # deve vir vazio
agent-browser network requests | grep -E " (4[0-9]{2}|5[0-9]{2})$" # deve vir vazio
```
- Qualquer erro de console JS ⇒ FALHOU (anote a mensagem).
- Qualquer request com status ≥ 400 ⇒ FALHOU (anote a URL — um asset
`*_controller-*.js` com 404 significa Stimulus quebrado na página inteira; foi
exatamente assim que o bug do preview do romaneio passou despercebido).
- Exceção: chamadas para domínios de terceiros (cloudflareinsights etc.) com
falha não reprovam o caso; registre como observação.
## Roteiro
### Caso 1 — Login por e-mail
```bash
agent-browser open https://teste.reemtransportes.com.br/auth/login
agent-browser snapshot -i # localizar refs de E-mail, Senha e Entrar
agent-browser fill @eX "admin@reem.com"
agent-browser fill @eY "Reem@2026!"
agent-browser press Enter
agent-browser wait --load networkidle
agent-browser get url
```
**PASSOU se**: a URL final é `/dashboard` (ou a home do perfil) e o Teste 0 passa.
Obs.: use `press Enter` — o clique no botão às vezes não navega no primeiro clique.
### Caso 2 — Dashboards
```bash
agent-browser open https://teste.reemtransportes.com.br/dashboard
agent-browser eval "document.querySelectorAll('canvas').length" # gráficos Chart.js
agent-browser eval "document.documentElement.scrollWidth <= window.innerWidth" # sem overflow horizontal
agent-browser screenshot dashboard.png
agent-browser open https://teste.reemtransportes.com.br/dashboard/operacoes
# repetir os três comandos acima (operacoes.png)
```
**PASSOU se**: as duas páginas carregam, há pelo menos 1 `canvas` em cada, sem
overflow horizontal, Teste 0 limpo.
**Versão mobile** (obrigatória no dashboard):
```bash
agent-browser set device "iPhone 16"
agent-browser open https://teste.reemtransportes.com.br/dashboard
agent-browser eval "document.documentElement.scrollWidth <= window.innerWidth"
agent-browser screenshot dashboard-mobile.png
agent-browser set viewport 1280 800 # voltar ao desktop
```
**PASSOU se**: sem scroll horizontal no mobile (regra do projeto: nada de tabela
que exija zoom).
### Caso 3 — Consolidações (abas e filtros)
```bash
agent-browser open "https://teste.reemtransportes.com.br/consolidacoes?visao=lista"
agent-browser snapshot -i | head -40
agent-browser open "https://teste.reemtransportes.com.br/consolidacoes?visao=motoristas"
agent-browser get url
```
**PASSOU se**: as duas visões abrem sem erro; ao trocar de visão os filtros da URL
se mantêm (as visões são abas da MESMA tela, não telas separadas); Teste 0 limpo.
### Caso 4 — Romaneios (inclui o preview)
```bash
agent-browser open https://teste.reemtransportes.com.br/admin/romaneios
agent-browser snapshot -i | head -30 # lista de romaneios
# abrir o primeiro romaneio da lista (link do item):
agent-browser click @eX
agent-browser wait --load networkidle
```
Na tela do romaneio:
```bash
# 4a. O modal de prévia NÃO pode estar visível no carregamento:
agent-browser eval "const o=document.querySelector('[data-romaneio-target=overlay]'); o?getComputedStyle(o).display:'sem overlay'"
# esperado: "none"
# 4b. Abrir a prévia:
agent-browser snapshot -i | grep -i "prévia" # achar o botão "Ver prévia do PDF"
agent-browser click @eY
agent-browser eval "const o=document.querySelector('[data-romaneio-target=overlay]'); getComputedStyle(o).display + ' src:' + (document.querySelector('iframe[data-romaneio-target=preview]')?.src||'')"
# esperado: "flex src:https://.../pdf?veiculo=..."
# 4c. Fechar com Esc:
agent-browser press Escape
agent-browser eval "getComputedStyle(document.querySelector('[data-romaneio-target=overlay]')).display"
# esperado: "none"
# 4d. PDF responde:
agent-browser eval "fetch(document.querySelector('a[href*=pdf]').href).then(r=>r.status)"
# esperado: 200
```
**PASSOU se**: 4a="none", 4b="flex" com src preenchido, 4c="none", 4d=200,
Teste 0 limpo. **NÃO** clicar em "Reimportar plano" (tem confirmação).
### Caso 5 — Planilha SimpliRoute
```bash
agent-browser open https://teste.reemtransportes.com.br/admin/planilha_simpli_route
agent-browser snapshot -i | head -20
```
**PASSOU se**: a tela abre com a lista de operações disponíveis, Teste 0 limpo.
(Não é preciso baixar o arquivo.)
### Caso 6 — Telas de admin
Abrir cada URL e rodar o Teste 0:
```
/admin/usuarios
/admin/perfis
/admin/configuracoes
/admin/auditoria_logs
/admin/edicao_lancamento
```
**PASSOU se**: todas abrem com conteúdo (título + tabela/cards), sem erro.
**NÃO** editar nada nessas telas.
### Caso 7 — Notificações
Abrir cada URL e rodar o Teste 0:
```
/admin/contatos
/admin/grupos
/admin/eventos
/admin/envios
```
**PASSOU se**: todas abrem, sem erro. **NÃO** disparar envio nem mexer no
WhatsApp/sessão.
### Caso 8 — Login PIN do motorista (mobile)
Somente se um PIN de teste foi fornecido na tarefa; senão marcar `PULADO`.
```bash
agent-browser set device "iPhone 16"
agent-browser open https://teste.reemtransportes.com.br/auth/login
# clicar na aba "PIN Motorista", digitar o PIN, entrar
```
**PASSOU se**: cai no dashboard do motorista, valores legíveis sem zoom,
Teste 0 limpo. Ao final: `agent-browser set viewport 1280 800`.
## Relatório final (obrigatório)
Terminar a execução com uma tabela e a lista de falhas:
| Caso | Resultado | Evidência |
|------|-----------|-----------|
| 0 (por página) | PASSOU/FALHOU | página + erro |
| 1 Login | PASSOU/FALHOU/DEPLOY?/PULADO | screenshot |
| 2 Dashboards | … | … |
| 3 Consolidações | … | … |
| 4 Romaneios/preview | … | … |
| 5 Planilha | … | … |
| 6 Admin | … | … |
| 7 Notificações | … | … |
| 8 PIN motorista | … | … |
Depois da tabela, listar **somente as falhas**, cada uma com: URL, o que era
esperado, o que aconteceu (mensagem de erro/console) e o caminho do screenshot.
Se tudo passou, dizer isso em uma linha. Encerrar com `agent-browser close`.