From c454cb8cc79193f3c01a86efe615ed8c04cbd6c211cc34bdbde86fb9ce0bed2d Mon Sep 17 00:00:00 2001 From: victor Date: Fri, 28 Aug 2026 09:40:59 -0300 Subject: [PATCH] =?UTF-8?q?Cria=C3=A7=C3=A3o=20da=20bateria=20de=20testes?= =?UTF-8?q?=20automatizados,=20corre=C3=A7=C3=A3o=20da=20pagina=20de=20ger?= =?UTF-8?q?ar=20romaneio=20e=20plano=20da=20cria=C3=A7=C3=A3o=20do=20modul?= =?UTF-8?q?o=20cliente?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 195 ++++++++++++++++ .../controllers/romaneio_controller.js | 7 + app/views/admin/romaneios/show.html.erb | 11 +- docs/BATERIA-DE-TESTES.md | 212 ++++++++++++++++++ 4 files changed, 423 insertions(+), 2 deletions(-) create mode 100644 docs/BATERIA-DE-TESTES.md diff --git a/README.md b/README.md index de9f8cf..2c6f37e 100644 --- a/README.md +++ b/README.md @@ -3513,3 +3513,198 @@ Dockerfile # tira a linha de precompile mort ``` + +--- + +
+🆕 Atualização 28/08/2026 — PLANO: Módulo do Cliente (aprovado, NÃO implementado) + +> ⚠️ **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 48–56px), 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. + +
diff --git a/app/javascript/controllers/romaneio_controller.js b/app/javascript/controllers/romaneio_controller.js index e986b15..e52f359 100644 --- a/app/javascript/controllers/romaneio_controller.js +++ b/app/javascript/controllers/romaneio_controller.js @@ -133,7 +133,12 @@ export default class extends Controller { abrirPreview() { 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.classList.remove("hidden") + this.overlayTarget.classList.add("flex") // Trava o scroll do fundo: rolar a lista atrás de um modal aberto faz o // operador perder o lugar quando fecha. document.body.style.overflow = "hidden" @@ -144,6 +149,8 @@ export default class extends Controller { if (!this.hasOverlayTarget || this.overlayTarget.hidden) return this.overlayTarget.hidden = true + this.overlayTarget.classList.remove("flex") + this.overlayTarget.classList.add("hidden") document.body.style.overflow = "" } diff --git a/app/views/admin/romaneios/show.html.erb b/app/views/admin/romaneios/show.html.erb index 996ba70..1804a49 100644 --- a/app/views/admin/romaneios/show.html.erb +++ b/app/views/admin/romaneios/show.html.erb @@ -92,10 +92,17 @@