2591 lines
152 KiB
Markdown
2591 lines
152 KiB
Markdown
# 🚛 Reem Logística — Sistema de Controle de Custos
|
||
|
||
Sistema web para controle de custos de entregas hospitalares da **Gade Hospitalar**.
|
||
|
||
**Stack:** Ruby on Rails 7+ · PostgreSQL 15 · Docker · Tailwind CSS · Hotwire (Turbo + Stimulus)
|
||
**Repositório:** https://git.xenserver.com.br/Cludio-code/logistica-controle-custos
|
||
|
||
---
|
||
|
||
> 💡 **Navegação:** as seções abaixo abrem e fecham — clique no título de cada uma.
|
||
|
||
---
|
||
|
||
<details>
|
||
<summary><strong>📋 CONTEXTO DO PROJETO (leia antes de tudo)</strong></summary>
|
||
|
||
A **Gade Hospitalar** precisa controlar o custo de cada operação de entrega
|
||
(UBS SUL, UBS LESTE, EMAD, STS, SAD, etc.).
|
||
|
||
O banco **já existe** e é atualizado a cada 1 hora por outro sistema externo:
|
||
- Tabela: `public.db_reem_simplerout_2026`
|
||
- Account: Gade Hospitalar (account_id: 95907)
|
||
- **NUNCA** fazer `DROP TABLE`, `TRUNCATE`, `DELETE` ou migration nessa tabela.
|
||
|
||
O sistema novo cria suas próprias tabelas no mesmo PostgreSQL (schema separado ou prefixo)
|
||
e lê a tabela existente apenas para exibir e consolidar entregas.
|
||
|
||
</details>
|
||
|
||
---
|
||
|
||
<details>
|
||
<summary><strong>🏗️ Estado atual — O que já foi feito (Fase 1 ✅)</strong></summary>
|
||
|
||
```
|
||
logistica-controle-custos/
|
||
├── Gemfile # Todas as gems (Devise, Pundit, Prawn, Whenever, Tailwind…)
|
||
├── Dockerfile # Ruby 3.2.2-slim
|
||
├── docker-compose.yml # app + db (postgres:15)
|
||
├── tailwind.config.js # Paleta preto #0a0a0a / laranja #f97316 / branco
|
||
├── .env.example # Template de variáveis — copiar para .env
|
||
├── .gitignore # Protege .env, master.key, credenciais
|
||
│
|
||
├── config/
|
||
│ ├── database.yml # 100% via ENV — nunca hardcode
|
||
│ └── routes.rb # Rotas completas para todas as 8 fases
|
||
│
|
||
├── app/
|
||
│ ├── models/
|
||
│ │ ├── entrega.rb # ⚠️ READ-ONLY — tabela existente db_reem_simplerout_2026
|
||
│ │ ├── user.rb # Devise + roles (admin/gerente/operador/motorista) + PIN
|
||
│ │ ├── configuracao.rb # Preços configuráveis (entrega, retirada, bonus, desconto)
|
||
│ │ ├── consolidacao.rb # Soft delete, enum status, cálculo de progresso
|
||
│ │ ├── consolidacao_motorista.rb
|
||
│ │ ├── consolidacao_entrega.rb # Enum tipo: normal/retirada/bonus/desconto
|
||
│ │ ├── historico_estimado.rb # Job 1h + cálculo ao vivo
|
||
│ │ └── auditoria_log.rb # Log de ações críticas
|
||
│ │
|
||
│ ├── controllers/
|
||
│ │ └── application_controller.rb # Auth + Pundit + tema
|
||
│ ├── policies/
|
||
│ │ └── application_policy.rb # Base Pundit
|
||
│ ├── helpers/
|
||
│ │ └── application_helper.rb # nav_link_to, moeda(), badge_status(), progress_bar()
|
||
│ └── views/layouts/
|
||
│ ├── application.html.erb # Layout base dark theme laranja/preto/branco
|
||
│ └── _navbar.html.erb # Sidebar responsiva com hamburguer mobile
|
||
│
|
||
└── db/
|
||
├── seeds.rb # Admin: admin@gade.com / Gade@2026! + configs de preço
|
||
└── migrate/
|
||
├── ..._create_users.rb
|
||
├── ..._create_configuracoes.rb
|
||
├── ..._create_consolidacoes.rb
|
||
├── ..._create_consolidacao_motoristas.rb
|
||
├── ..._create_consolidacao_entregas.rb
|
||
├── ..._create_historico_estimados.rb
|
||
└── ..._create_auditoria_logs.rb
|
||
```
|
||
|
||
</details>
|
||
|
||
---
|
||
|
||
<details>
|
||
<summary><strong>🚀 Setup do zero (próximo dev)</strong></summary>
|
||
|
||
### 1. Clone
|
||
```bash
|
||
git clone https://git.xenserver.com.br/Cludio-code/logistica-controle-custos.git
|
||
cd logistica-controle-custos
|
||
```
|
||
|
||
### 2. Configure o .env
|
||
```bash
|
||
cp .env.example .env
|
||
nano .env # preencha DB_HOST, DB_PASSWORD, etc.
|
||
```
|
||
|
||
### 3. Gere o SECRET_KEY_BASE
|
||
```bash
|
||
docker run --rm ruby:3.2.2-slim bash -c "gem install rails --no-doc -q && rails secret"
|
||
# Cole o resultado no .env → SECRET_KEY_BASE=...
|
||
```
|
||
|
||
### 4. Suba os containers
|
||
```bash
|
||
docker-compose up -d
|
||
```
|
||
|
||
### 5. Banco + seeds
|
||
```bash
|
||
docker-compose exec app bundle exec rails db:create db:migrate db:seed
|
||
```
|
||
|
||
### 6. Acesse
|
||
```
|
||
http://localhost:3000
|
||
Login: admin@gade.com
|
||
Senha: Gade@2026! ← ALTERE NO PRIMEIRO ACESSO
|
||
```
|
||
|
||
</details>
|
||
|
||
---
|
||
|
||
<details>
|
||
<summary><strong>🔧 Comandos do dia a dia</strong></summary>
|
||
|
||
```bash
|
||
docker-compose exec app bundle exec rails db:migrate # nova migration
|
||
docker-compose exec app bundle exec rails console # console Rails
|
||
docker-compose logs -f app # logs em tempo real
|
||
docker-compose down # parar tudo
|
||
```
|
||
|
||
</details>
|
||
|
||
---
|
||
|
||
<details>
|
||
<summary><strong>🗺️ Fases de implementação</strong></summary>
|
||
|
||
| Fase | Status | O que faz |
|
||
|------|--------|-----------|
|
||
| **1** | ✅ Concluída | Setup + Docker + Models + Migrations + Seeds + Layout base |
|
||
| **2** | ✅ Concluída *(corrigida)* | Login e-mail/PIN + CRUD usuários + pilares de preço + toggle tema |
|
||
| **3** | ✅ Concluída *(corrigida)* | Dashboard: cards + gráfico Chart.js + ranking motoristas + navegação por mês |
|
||
| **4** | ✅ Concluída | Job Whenever (1h) + HistoricoEstimado + AuditoriaLog + API métricas |
|
||
| **5** | ✅ Concluída | Nova Consolidação + filtros + Wizard Passo 1 |
|
||
| **6** | ✅ Concluída | Toggles coloridos + ações em massa + Wizard Passos 2 e 3 (Stimulus) |
|
||
| **7** | ✅ Concluída | PDFs Prawn: relatório + extrato com QR code + modal preview |
|
||
| **8** | ✅ Concluída | Painel motorista + login PIN/QR + notificações WhatsApp/email |
|
||
|
||
**🎉 PROJETO: 8 de 8 fases implementadas.**
|
||
|
||
</details>
|
||
|
||
---
|
||
|
||
<details>
|
||
<summary><strong>🔧 Análise de integração (11/06/2026) — Correções aplicadas</strong></summary>
|
||
|
||
As Fases 2/3 (commit `4724847`) foram desenvolvidas em paralelo às Fases 4-8 (commits `90ed705` e `8b3d4bc`), e a mesclagem gerou **conflitos que quebravam a aplicação**. Análise dos commits e correções:
|
||
|
||
### 🔴 Problemas críticos encontrados e corrigidos
|
||
|
||
**1. Migration da Fase 2 quebrada** (`20260610_add_fase2_fields_to_users.rb`)
|
||
- Timestamp curto (8 dígitos) fazia ela rodar ANTES da `create_users` → erro "table does not exist"
|
||
- Adicionava coluna `ativo` que já existia → erro de coluna duplicada
|
||
- Criava `pin_acesso` duplicando o `pin_code` existente
|
||
- ✅ **Corrigida:** reescrita como `20260101000010_add_devise_trackable_to_users.rb` — só trackable/lockable, timestamp correto
|
||
|
||
**2. Campo de PIN inconsistente** — Fase 2 usava `pin_acesso`, o model e as Fases 7/8 usam `pin_code`. PINs cadastrados no CRUD não funcionariam no login.
|
||
- ✅ **Corrigido:** padronizado `pin_code` em todos os arquivos
|
||
|
||
**3. Campo de nome inconsistente** — CRUD de usuários usava `:name`, o schema usa `nome` → o CRUD quebrava ao salvar.
|
||
- ✅ **Corrigido:** padronizado `nome`
|
||
|
||
**4. Controllers fora do namespace** — As rotas esperam `Admin::UsuariosController` e `Admin::ConfiguracoesController`, mas os controllers foram criados na raiz → `/admin/usuarios` dava "uninitialized constant" e os controllers ficavam inacessíveis.
|
||
- ✅ **Corrigido:** movidos para `app/controllers/admin/` + views para `app/views/admin/` + paths atualizados (`admin_usuarios_path` etc.)
|
||
|
||
**5. Dashboard (Fase 3) chamava métodos inexistentes** — `Entrega.do_mes` e `Configuracao.mapa_de_precos` foram sobrescritos pelo commit das Fases 4-6 → **a raiz do site quebrava** (root = dashboard#index).
|
||
- ✅ **Corrigido:** scope `do_mes` e método `mapa_de_precos` restaurados nos models, junto com `tipo_label`/`icone` usados pelas views de configurações
|
||
|
||
**6. AuditoriaLog com 2 assinaturas** — Fase 2 chamava `registrar(user, 'acao', ip)` posicional; o model usa keyword args → `ArgumentError` em login PIN, CRUD de usuários e configurações.
|
||
- ✅ **Corrigido:** todas as chamadas convertidas para a assinatura do model
|
||
|
||
**7. Rotas/controllers ausentes**
|
||
- `Admin::AuditoriaLogsController` tinha rota mas não existia → ✅ criado com view de listagem e filtros
|
||
- `toggle_tema` e `toggle_ativo` existiam nos controllers mas sem rota → ✅ rotas adicionadas
|
||
- Controller de configurações usava campo `tipo` que não existe (schema usa `chave`) → ✅ corrigido
|
||
|
||
### 🟡 Duplicações removidas (arquivos órfãos)
|
||
- `app/controllers/motorista_controller.rb` + `app/views/motorista/index.html.erb` — a rota `/motorista` usa `Motorista::DashboardController` (Fase 8)
|
||
- `app/views/layouts/_sidebar.html.erb` — o layout renderiza `_navbar.html.erb`
|
||
- Seeds dos 3 motoristas de teste (João/1234, Maria/5678, Carlos/9012) restaurados — tinham sido sobrescritos
|
||
|
||
### ⚠️ Observação sobre login PIN duplicado
|
||
Existem **dois fluxos de login por PIN** funcionais: a aba PIN na tela Devise (`/auth/login`, Fase 2) e a tela dedicada (`/motorista/login`, Fase 8 — usada pelo QR Code do extrato). Ambos usam `pin_code` e funcionam; recomenda-se manter os dois (a tela dedicada é melhor para mobile) ou unificar futuramente.
|
||
|
||
### 📂 Arquivos das Fases 4, 5 e 6
|
||
|
||
**Fase 4 — Job 1h + Auditoria:**
|
||
```
|
||
config/schedule.rb # Whenever — roda a cada 1h
|
||
lib/tasks/historico.rake # rake historico:atualizar
|
||
app/jobs/application_job.rb
|
||
app/jobs/atualizar_historico_estimado_job.rb # conta entregas pagas × preco_entrega
|
||
app/controllers/api/v1/dashboard_controller.rb # GET /api/v1/dashboard/metricas
|
||
app/controllers/concerns/auditavel.rb # auditar!(:acao, registro) nos controllers
|
||
app/policies/dashboard_policy.rb
|
||
```
|
||
Ativar o cron no servidor: `bundle exec whenever --update-crontab`
|
||
Testar manualmente: `docker-compose exec app bundle exec rake historico:atualizar`
|
||
|
||
**Fase 5 — Consolidação + Wizard Passo 1:**
|
||
```
|
||
db/migrate/20260101000008_add_route_ids_to_consolidacoes.rb # rodar db:migrate!
|
||
app/controllers/consolidacoes_controller.rb # index/new/create/show/wizard/finalizar/arquivar
|
||
app/policies/consolidacao_policy.rb
|
||
app/views/consolidacoes/index.html.erb # lista com filtros (status/nome/período/motorista/rota)
|
||
app/views/consolidacoes/new.html.erb # form: nome + 2 date pickers + multi-select motoristas/rotas
|
||
app/views/consolidacoes/wizard.html.erb # Passo 1: lista motoristas com progresso
|
||
```
|
||
|
||
**Fase 6 — Validação + Toggles + Massa:**
|
||
```
|
||
app/controllers/consolidacao_entregas_controller.rb # validar/classificar/classificar_em_massa/revisar
|
||
app/views/consolidacao_entregas/validar.html.erb # Passo 2: toggles coloridos + checkbox massa + resumo lateral
|
||
app/views/consolidacao_entregas/revisar.html.erb # Passo 3: resumo + próximo motorista / salvar rascunho
|
||
app/javascript/controllers/validacao_controller.js # Stimulus: tempo real (progresso, resumo, toggles)
|
||
config/routes.rb # ATUALIZADO — rotas do wizard
|
||
```
|
||
|
||
**Cores dos toggles (padrão do projeto):**
|
||
🟧 Laranja = Entrega Normal · 🟫 Laranja escuro = Retirada · ⬜ Branco = Bônus · ⬛ Preto = Desconto
|
||
|
||
**Regras implementadas:**
|
||
- Entregas elegíveis: `status='completed'` AND `checkout IS NOT NULL` (somente leitura da tabela existente)
|
||
- Não permite finalizar consolidação com entregas não classificadas (botão desabilitado + validação server-side)
|
||
- Consolidação finalizada não pode ser editada por operadores (Pundit)
|
||
- Todas ações críticas registradas em `auditoria_logs`
|
||
|
||
**Fase 7 — PDFs (Prawn + QR Code):**
|
||
```
|
||
Gemfile # + rqrcode, chunky_png
|
||
db/migrate/20260101000009_add_login_token_to_users.rb # token para QR — rodar db:migrate!
|
||
app/services/pdf/base_pdf.rb # layout base: cabeçalho preto/laranja + rodapé
|
||
app/services/pdf/relatorio_motorista_pdf.rb # tabela detalhada de entregas + resumo + total
|
||
app/services/pdf/extrato_pdf.rb # estilo contracheque + QR Code de login rápido
|
||
app/controllers/consolidacoes_controller.rb # ATUALIZADO: gerar_pdf_relatorio/gerar_pdf_extrato/preview_extrato
|
||
app/views/consolidacoes/show.html.erb # botões por motorista + modal de preview
|
||
app/views/consolidacoes/preview_extrato.html.erb # preview HTML antes de confirmar o PDF
|
||
app/models/user.rb # ATUALIZADO: regenerar_login_token!
|
||
```
|
||
Soft delete já implementado: consolidações finalizadas vão para "arquivada", nunca são excluídas.
|
||
|
||
**Fase 8 — Painel Motorista + Notificações:**
|
||
```
|
||
Gemfile # + twilio-ruby
|
||
app/controllers/motorista/sessoes_controller.rb # login PIN 4 dígitos + acesso via QR (/motorista/acesso/:token)
|
||
app/controllers/motorista/dashboard_controller.rb # 2 cards: estimado (mês) + consolidado (finalizadas)
|
||
app/views/motorista/sessoes/new.html.erb # teclado numérico gigante mobile-first (envia no 4º dígito)
|
||
app/views/motorista/dashboard/index.html.erb # cards laranja/branco + lista com download do próprio extrato
|
||
app/services/notificacao_service.rb # WhatsApp (Twilio) + email — falha nunca quebra a finalização
|
||
app/mailers/application_mailer.rb
|
||
app/mailers/consolidacao_mailer.rb # pagamento_fechado
|
||
app/views/consolidacao_mailer/pagamento_fechado.html.erb # email com identidade visual da marca
|
||
app/views/layouts/mailer.html.erb
|
||
config/initializers/smtp.rb # SMTP via ENV (só ativa se SMTP_USERNAME existir)
|
||
config/routes.rb # ATUALIZADO: /motorista/login + /motorista/acesso/:token
|
||
```
|
||
|
||
**Notificações — como ativar:**
|
||
1. WhatsApp: preencha `TWILIO_ACCOUNT_SID`, `TWILIO_AUTH_TOKEN` e `TWILIO_WHATSAPP_FROM` no `.env` e mude a configuração `notificacao_whatsapp` para `true` no sistema. Motorista precisa ter `telefone` cadastrado (formato `+5511999998888`).
|
||
2. Email: preencha as variáveis `SMTP_*` no `.env` e mude `notificacao_email` para `true`. Motorista precisa ter `email`.
|
||
3. Ao finalizar uma consolidação, cada motorista recebe automaticamente a mensagem com o valor e o link do painel.
|
||
|
||
**Fluxo do motorista (público de baixo nível técnico):**
|
||
- Acessa `/motorista/login` → digita PIN de 4 números num teclado gigante (envia sozinho no 4º dígito)
|
||
- OU escaneia o QR Code impresso no extrato → entra direto sem digitar nada
|
||
- Vê 2 cards: 🟧 valor estimado do mês ("aproximado") e ⬜ valor fechado para pagamento
|
||
- Baixa o próprio extrato das consolidações finalizadas
|
||
|
||
### 🧪 Teste end-to-end sugerido
|
||
1. Login admin → criar consolidação (nome + período + motoristas)
|
||
2. Wizard Passo 1 → escolher motorista → Passo 2: classificar entregas com os toggles
|
||
3. Usar "Marcar todos como Normal" → Passo 3: revisar → próximo motorista
|
||
4. Com tudo classificado → Finalizar (motoristas notificados)
|
||
5. Na tela da consolidação → "Ver antes de gerar" → Confirmar → baixar Extrato PDF
|
||
6. Logout → `/motorista/login` → entrar com PIN do motorista
|
||
7. Conferir cards de valores → baixar próprio extrato → sair
|
||
|
||
### ▶️ Próximos passos (deploy)
|
||
|
||
O projeto está completo. Para colocar em produção:
|
||
|
||
```bash
|
||
# 1. Configurar .env com credenciais reais (DB_HOST do PostgreSQL existente)
|
||
cp .env.example .env && nano .env
|
||
|
||
# 2. Subir e migrar
|
||
docker-compose up -d --build
|
||
docker-compose exec app bundle exec rails db:create db:migrate db:seed
|
||
|
||
# 3. Ativar o cron do job de 1h
|
||
docker-compose exec app bundle exec whenever --update-crontab
|
||
|
||
# 4. Testar o fluxo end-to-end (seção de teste acima)
|
||
```
|
||
|
||
**Credenciais de teste (seeds):**
|
||
|
||
| Usuário | Login | Senha/PIN |
|
||
|---------|-------|-----------|
|
||
| Admin | admin@gade.com | Gade@2026! |
|
||
| Gerente | gerente@gade.com | Gade@2026! |
|
||
| Motorista João Silva | PIN | 1234 |
|
||
| Motorista Maria Santos | PIN | 5678 |
|
||
| Motorista Carlos Souza | PIN | 9012 |
|
||
|
||
⚠️ Altere as senhas padrão no primeiro acesso em produção.
|
||
|
||
</details>
|
||
|
||
---
|
||
|
||
<details>
|
||
<summary><strong>🧪 Auditoria + Suíte de testes (15/06/2026)</strong></summary>
|
||
|
||
Revisão de código + criação da suíte RSpec (o projeto tinha `rspec-rails`/`factory_bot`/`capybara` no Gemfile, mas **nenhum teste** e **nenhum `spec/`**).
|
||
|
||
### 🔴 Bugs corrigidos
|
||
|
||
**1. Erro de sintaxe em `Admin::UsuariosController`** — `toggle_ativo` tinha `entidade: \'User\'` (aspas escapadas com barra). O arquivo não carregava → qualquer acesso a `/admin/usuarios` dava 500. ✅ corrigido.
|
||
|
||
**2. Migration quebrava o setup do zero** — `20260610_add_fase2_fields_to_users.rb` readicionava a coluna `ativo` (já criada em `create_users`) sem guard → `PG::DuplicateColumn` num `db:migrate` limpo. ✅ guard `unless column_exists?` em `ativo` e `pin_acesso`.
|
||
|
||
**3. `db:seed` falhava no 2º motorista** — motoristas sem e-mail colidiam no índice único de `email` (`default ""`). ✅ seeds agora geram placeholder único (`nome.parameterize@motorista.local`).
|
||
|
||
**4. Dashboard ignorava o filtro de conta Gade** — `DashboardController` e `HistoricoEstimado.{atualizar!,calcular_ao_vivo}` contavam entregas de **todas** as contas. ✅ aplicado `.da_conta_gade`.
|
||
|
||
**5. `valor_total` com duas fontes de verdade** — o controller somava `valor_aplicado` (capturado na classificação) e o callback do model recalculava pelos **preços atuais** de `Configuracao` → divergência quando os preços mudavam. ✅ `ConsolidacaoMotorista#recalcular_valor` agora usa `valor_aplicado` (fonte única).
|
||
|
||
**6. Gate de finalização + classificação sem validação**
|
||
- `todas_entregas_classificadas?` usava soma agregada com `>=`, mascarando motoristas pendentes quando outro tinha entregas de sobra. ✅ agora exige cada motorista completo (`all?`) e respeita o filtro de rotas.
|
||
- `classificar`/`classificar_em_massa` aceitavam `tracking_id`/`motorista` arbitrários. ✅ validam que o motorista pertence à consolidação e que a entrega é elegível no período/rota.
|
||
|
||
**7. Coluna órfã `pin_acesso`** — nunca usada (o sistema usa `pin_code`). ✅ removida via migration.
|
||
|
||
**Minors:** motorista logado conseguia abrir `/dashboard` (✅ redireciona ao painel do motorista); `Date.parse(params[:data])` sem rescue dava 500 (✅ cai em `Date.current`); `arquivar!` não registrava autor (✅ novas colunas `arquivado_por`/`arquivado_em` + `belongs_to :arquivador`); `MotoristaController` + `app/views/motorista/index.html.erb` órfãos e quebrados (`current_user.name`, `where(user:)`, status inexistentes) — ✅ removidos.
|
||
|
||
### 🆕 Migrations adicionadas (rodar `db:migrate`)
|
||
```
|
||
db/migrate/20260615000001_remove_pin_acesso_from_users.rb
|
||
db/migrate/20260615000002_add_arquivamento_to_consolidacoes.rb # arquivado_por / arquivado_em
|
||
```
|
||
|
||
### ✅ Suíte de testes
|
||
```
|
||
.rspec
|
||
config/database.yml # ENV-driven, com banco de teste separado (<DB_NAME>_test)
|
||
spec/spec_helper.rb · spec/rails_helper.rb
|
||
spec/support/{pundit,devise}.rb
|
||
spec/factories.rb
|
||
spec/models/{user,configuracao,consolidacao,consolidacao_motorista,consolidacao_entrega,auditoria_log}_spec.rb
|
||
spec/policies/consolidacao_policy_spec.rb
|
||
spec/requests/dashboard_spec.rb
|
||
```
|
||
|
||
Como rodar:
|
||
```bash
|
||
docker-compose up -d
|
||
docker-compose exec app env RAILS_ENV=test bundle exec rails db:create db:migrate
|
||
docker-compose exec app bundle exec rspec
|
||
```
|
||
|
||
> Os specs que dependem da tabela read-only `db_reem_simplerout_2026` stubam `Entrega` — o banco de teste não contém essa tabela externa.
|
||
|
||
</details>
|
||
|
||
---
|
||
|
||
<details>
|
||
<summary><strong>🔧 Correções + funcionalidades (16/06/2026)</strong></summary>
|
||
|
||
### 🔴 Erros de runtime corrigidos (reportados em prints)
|
||
- **Logout** dava `No route matches [GET] /auth/logout` — o link usava `method: :delete` (rails-ujs), ignorado pelo Turbo. Agora usa `data-turbo-method`.
|
||
- **/admin/usuarios** e **/admin/configuracoes** estouravam `NoMethodError: admin_ou_gerente?` — helpers de papel (`admin?`, `admin_ou_gerente?`) movidos para a `ApplicationPolicy`.
|
||
- **Listagem de usuários** chamava o helper inexistente `badge_role` — criado em `ApplicationHelper`.
|
||
- **Nova consolidação** estourava `NoMethodError: new?` — `ApplicationPolicy` agora define `new? = create?` e `edit? = update?`.
|
||
- **Login do motorista (PIN)** derrubava `/motorista` com `PG::UndefinedTable: relation "consolidacao_motorista" does not exist` — o inflector pt-BR resolvia o composto no singular. `self.table_name` fixado explicitamente em `Consolidacao`, `ConsolidacaoMotorista`, `ConsolidacaoEntrega` e `Configuracao`.
|
||
- **Listagem de usuários** faltava o helper `badge_status_usuario` e a rota `toggle_ativo` estava sem o prefixo `admin_` (verbo errado também) — corrigidos.
|
||
- **Passo 3 (revisão) da consolidação** ficava em branco: `GROUP BY tipo` combinado com `ORDER BY created_at` é inválido no PostgreSQL (`PG::GroupingError`, vira 500/tela branca em produção). Corrigido com `reorder(nil)` no count agrupado.
|
||
|
||
### 🆕 Funcionalidades
|
||
- **Dashboard com filtro de faixa de datas** (Flatpickr, modo range) no lugar da navegação por mês; o controller passou a consultar por período e a respeitar `.da_conta_gade`.
|
||
- **Regra de pagamento por checkout:** `Entrega.pagas` agora é `status='completed' AND checkout IS NOT NULL` (antes era `checkin`). Vale para dashboard, painel do motorista, job de 1h e elegibilidade das consolidações.
|
||
- **Consolidação por veículo:** o filtro/identificação passou de rota (`route_id`, UUID) para **veículo** (`vehicle`). Migration troca `consolidacoes.route_ids` por `vehicle_ids`.
|
||
- **Filtro de conta configurável:** `account_id` na tabela externa é texto e vinha vazio nos registros recentes (o dado de junho estava 100% em conta vazia, e o filtro fixo em `95907` zerava o dashboard). `DB_EXISTING_ACCOUNT_ID` agora aceita lista de contas ou `all`/vazio (sem filtro). Comparação feita como texto.
|
||
- **Múltiplos pilares por entrega:** na validação da consolidação cada entrega pode receber mais de um pilar ao mesmo tempo (ex.: Normal + Bônus + Retirada), somando os valores. Os toggles viraram multi-seleção (ligam/desligam por pilar); a unicidade passou a ser `[consolidacao_id, tracking_id, tipo]` e o progresso conta entregas distintas.
|
||
- **Tela de "Pilares de Preço"** passou a listar só os pilares de preço (Entrega/Retirada/Bônus/Desconto); Nome da Empresa e Notificações saíram dali (não são preço).
|
||
|
||
### ⚙️ Automação / diagnóstico
|
||
- `docker-compose.yml`: o boot roda `rails db:prepare` antes do servidor — **não precisa mais rodar `db:migrate` na mão** após o `up`.
|
||
- `rake reem:diagnostico`: checa a tabela externa por conta/período/status/checkout e o intervalo de datas disponível (útil para depurar dashboard vazio).
|
||
|
||
```bash
|
||
docker-compose exec app bundle exec rake reem:diagnostico
|
||
docker-compose exec app bundle exec rake reem:diagnostico INICIO=2026-02-01 FIM=2026-06-30
|
||
```
|
||
|
||
</details>
|
||
|
||
---
|
||
|
||
<details>
|
||
<summary><strong>🔧 Correções + funcionalidades (16/06/2026 — sessão 2)</strong></summary>
|
||
|
||
### 🔴 Bugs corrigidos
|
||
- **Dashboard zerado por filtro de conta:** `DB_EXISTING_ACCOUNT_ID` é uma **lista separada por vírgula**, não um trecho de SQL. Valor errado (ex.: `account_id IN ('95907', '')`) zerava tudo. Use `95907,` (vírgula no fim) para incluir também registros com conta vazia/NULL. O scope `Entrega.da_conta_gade` agora inclui `NULL` quando há token vazio na lista.
|
||
- **Dashboard travava ao voltar (só com F5):** era o cache de preview do Turbo servindo um snapshot congelado. Adicionado `turbo-cache-control: no-cache` na página e o gráfico Chart.js virou idempotente (`Chart.getChart(ctx)?.destroy()`).
|
||
- **500 ao criar motorista:** `users.email` era `NOT NULL` com índice único e default `""` — o 2º motorista sem e-mail colidia em `""` (`RecordNotUnique`). Agora a coluna permite `NULL` e e-mail em branco vira `nil` (`User#normalizar_email_em_branco`); no Postgres vários `NULL` convivem no índice único.
|
||
- **500 ao gerar relatório PDF** (`gerar_pdf_relatorio`): `@entregas.group(:tipo).count` com `.order(:created_at)` gerava `GROUP BY ... ORDER BY created_at` → `PG::GroupingError`. Corrigido com `reorder(nil)`. Também removidas as opções `color:` passadas ao `Prawn#text` (não suportadas — uso de `fill_color`).
|
||
- **Motorista não baixava o próprio extrato** ("sem permissão"): `ConsolidacaoPolicy#show?` exige `pode_consolidar?` (falso para motorista). Novo `autorizar_extrato!` no `ConsolidacoesController` — staff baixa qualquer um; motorista baixa só o próprio (PDF já filtrado pelo nome dele).
|
||
- **QR code do extrato não abria:** URL estava fixa em `https://`, mas o servidor roda em **http** (IP:porta). Novo `url_acesso` respeita o esquema do `APP_HOST` (sem esquema = http). Defina `APP_HOST` com o IP/host acessível pelo celular.
|
||
|
||
### 🆕 Funcionalidades
|
||
- **NF + Endereço no relatório PDF:** colunas separadas; a coluna **Tracking** foi substituída por **Veículo** (relatório e tela de revisão / passo 3).
|
||
- **Endereço nas telas da consolidação:** linha de endereço nos cards do passo 2 (validação) e coluna Endereço no passo 3 (revisão, paginado 20/50/100).
|
||
- **Campos de assinatura nos PDFs:** bloco com assinatura do **Motorista** e do **Administrador** (Reem Transporte) no relatório e no extrato (`BasePdf#assinaturas`).
|
||
- **Selecionar todos** no passo 2: checkbox que marca/desmarca todas as entregas de uma vez.
|
||
- **Desselecionar em massa:** toggle "Modo remover" — os botões Normal/Retirada/Bônus/Desconto (e "marcar todos") passam a **remover** o pilar das entregas selecionadas (`classificar_em_massa` aceita `acao: remover`).
|
||
- **Botões de voltar** entre os passos do wizard (2→1, 3→2) viraram botões visíveis (antes eram texto cinza discreto).
|
||
|
||
### 🆕 Migration adicionada (rodar `db:migrate`)
|
||
- `20260616000003_allow_null_email_for_motoristas` — `users.email` passa a permitir `NULL`; e-mails `""` existentes viram `NULL`.
|
||
|
||
```bash
|
||
docker-compose exec app bundle exec rails db:migrate
|
||
```
|
||
|
||
</details>
|
||
|
||
---
|
||
|
||
<details>
|
||
<summary><strong>🗄️ Banco existente — referência rápida</strong></summary>
|
||
|
||
| Campo | Uso |
|
||
|-------|-----|
|
||
| `tracking_id` | PK lógico da entrega |
|
||
| `reference_id` | Número da NF |
|
||
| `planned_date` | Data da entrega (filtro principal) |
|
||
| `driver` | Nome do motorista |
|
||
| `status` | `'completed'` = entrega concluída |
|
||
| `checkout` | NULL = motorista não finalizou (define elegibilidade de pagamento) |
|
||
| `vehicle` | Veículo da entrega (usado no filtro de consolidação) |
|
||
| `route_id` | UUID da operação (não usado no app — substituído por `vehicle`) |
|
||
| `contact_name` | Nome do local (UBS SACOMA, EMAD VELEIROS…) |
|
||
|
||
**Regra de pagamento:** `status = 'completed'` AND `checkout IS NOT NULL`
|
||
|
||
</details>
|
||
|
||
---
|
||
|
||
<details>
|
||
<summary><strong>🎨 Design — padrão da marca</strong></summary>
|
||
|
||
| Elemento | Cor | Hex |
|
||
|----------|-----|-----|
|
||
| Fundo | Preto | `#0a0a0a` |
|
||
| Cards | Cinza escuro | `#1a1a1a` |
|
||
| Destaque / botões | Laranja | `#f97316` |
|
||
| Textos | Branco | `#ffffff` |
|
||
| Sucesso | Verde | `#22c55e` |
|
||
| Erro/desconto | Vermelho | `#ef4444` |
|
||
|
||
- Fonte mínima 16px desktop / 18px mobile
|
||
- Botões mínimo 48px altura (acessibilidade touch)
|
||
- Tema escuro como padrão
|
||
|
||
</details>
|
||
|
||
---
|
||
|
||
<details>
|
||
<summary><strong>👥 Perfis de acesso</strong></summary>
|
||
|
||
| Perfil | Login | Acesso |
|
||
|--------|-------|--------|
|
||
| Admin | email + senha | Tudo |
|
||
| Gerente | email + senha | Dashboard + Consolidações + PDFs |
|
||
| Operador | email + senha | Consolidações (criar + validar) |
|
||
| Motorista | PIN 4 dígitos ou QR | Painel pessoal + baixar próprio PDF |
|
||
|
||
</details>
|
||
|
||
---
|
||
|
||
<details>
|
||
<summary><strong>🩺 Correção (11/06): container caía mostrando ajuda do "rails new"</strong></summary>
|
||
|
||
**Sintoma:** `docker-compose up` subia e o log mostrava o help do `rails new` em loop.
|
||
**Causa:** o repositório tinha os arquivos da aplicação (models/controllers/views) mas **não tinha o esqueleto de boot do Rails** — sem `config/application.rb` e `config.ru`, o comando `rails s` não reconhece a pasta como um app Rails e cai no modo "criar app novo".
|
||
|
||
**Arquivos adicionados nesta correção:**
|
||
```
|
||
config.ru # entrada Rack
|
||
config/boot.rb, environment.rb # boot do Rails
|
||
config/application.rb # módulo GadeLogistica + timezone SP + pt-BR
|
||
config/puma.rb # servidor web
|
||
config/environments/{development,production,test}.rb
|
||
config/importmap.rb # pins do Stimulus/Turbo
|
||
config/initializers/devise.rb # obrigatório para o devise_for das rotas
|
||
config/locales/pt-BR.yml # formatos de data brasileiros
|
||
bin/rails, bin/rake # executáveis
|
||
app/javascript/application.js # bootstrap Turbo + Stimulus
|
||
app/javascript/controllers/{application,index}.js
|
||
Gemfile # + gem propshaft (servir o JS via importmap)
|
||
app/views/layouts/application.html.erb # + javascript_importmap_tags (Stimulus não carregava!)
|
||
config/routes.rb # devise_for agora aponta para users/sessions (login PIN)
|
||
```
|
||
|
||
### Como subir agora (passo a passo completo)
|
||
|
||
```bash
|
||
# 1. Criar o .env (você NÃO tem ele ainda — é cópia do exemplo):
|
||
cp .env.example .env
|
||
|
||
# 2. Gerar a SECRET_KEY_BASE e colar no .env:
|
||
openssl rand -hex 64
|
||
nano .env # cole em SECRET_KEY_BASE= e preencha DB_HOST/DB_USER/DB_PASSWORD
|
||
|
||
# 3. Rebuild (Gemfile mudou — gem propshaft):
|
||
docker-compose build --no-cache
|
||
docker-compose up -d
|
||
|
||
# 4. Criar banco do sistema + migrar + popular:
|
||
docker-compose exec app bundle exec rails db:create db:migrate db:seed
|
||
|
||
# 5. Conferir:
|
||
docker-compose logs -f app # deve mostrar "Listening on http://0.0.0.0:3000"
|
||
# Acesse http://SEU_IP:3000 → admin@gade.com / Gade@2026!
|
||
```
|
||
|
||
</details>
|
||
|
||
---
|
||
|
||
<details>
|
||
<summary><strong>🔐 Segurança</strong></summary>
|
||
|
||
- **NUNCA** versione `.env`, `config/master.key` ou qualquer arquivo com senha
|
||
- Repositório **PRIVADO** em git.xenserver.com.br
|
||
- Use sempre `ENV['VARIAVEL']` no código Ruby
|
||
|
||
</details>
|
||
|
||
---
|
||
|
||
<details>
|
||
<summary><strong>🆕 Atualização 17/06/2026 — Pagamentos, Operações, Dashboard financeiro e UI</strong></summary>
|
||
|
||
Conjunto de melhorias para deixar o sistema pronto para entrega final.
|
||
|
||
### 💳 Status de pagamento (Pago / Parcial / Pendente)
|
||
- Migration `add_pagamento_to_consolidacao_motoristas`: colunas **`pago_em`**, **`pago_por`**,
|
||
**`forma_pagamento`** em `consolidacao_motoristas` (pagamento é **por motorista**).
|
||
- `ConsolidacaoMotorista`: `pago?`, `marcar_pago!(user, forma:)`, `cancelar_pagamento!`,
|
||
scopes `pagos`/`pendentes`, `belongs_to :pagador`.
|
||
- `Consolidacao#status_pagamento` (`:pendente`/`:parcial`/`:pago`), `valor_pago`, `valor_pendente`,
|
||
scopes `pagamento_pago`/`pagamento_pendente`/`pagamento_parcial` (filtro da lista).
|
||
- Tela **show**: marcar motorista individual **ou a operação inteira** (com `<select>` de forma);
|
||
estorno. Ações `registrar_pagamento`/`cancelar_pagamento` (admin/gerente).
|
||
- Helper `badge_pagamento`; filtro pago/pendente na **index**; selo no **extrato**.
|
||
- **Notificação de pagamento**: `NotificacaoService.notificar_pagamento` (WhatsApp + e-mail
|
||
`ConsolidacaoMailer#pagamento_efetuado`) — falhas só logam.
|
||
- **Aviso in-app no painel do motorista**: faixa "Pagamento confirmado" (últimos 7 dias) + selo
|
||
PAGO por consolidação.
|
||
|
||
### 🗂️ Consolidações por OPERAÇÃO (tabelas externas `gade_entregas_*`)
|
||
- PORO **`app/models/operacao.rb`**: descobre as tabelas via `information_schema`, com
|
||
**whitelist anti-injection** (`Operacao.sanitizar` + `quote_table_name`) — nunca interpolar
|
||
nome de tabela cru. `Operacao.todas`, `dados(tabelas)`, `agrupadas_por_mes`.
|
||
- `Entrega.da_operacoes(tabelas)`: junta por NF (`reference_id::text = nota_fiscal`).
|
||
- Coluna **`operacoes` (jsonb)** em `consolidacoes`; `Consolidacao.com_operacoes(ops)`.
|
||
- **Nova consolidação**: multi-seleção de operação **pré-preenche** motoristas/veículos/período
|
||
e **filtra** as entregas por NF; liberdade de marcar **um ou todos** os motoristas da operação.
|
||
- ENV opcional `OPERACOES_TABLE_PREFIX` (default `gade_entregas_`).
|
||
|
||
### 📊 Dashboard financeiro
|
||
- **Filtro de operação** no topo (multi-seleção, agrupada por mês, com "Limpar") aplicado a tudo.
|
||
- KPIs: **Custo total**, **Ticket médio/entrega**, **Pago**, **A pagar**.
|
||
- Gráficos (Chart.js): **custo por operação**, **composição por tipo**, **custo por motorista**,
|
||
**rosca pago × a pagar** + tabelas (pagos por data de pagamento, pendentes).
|
||
|
||
### 📅 Correção de data — usar `checkout` (data real), não `planned_date`
|
||
- `Entrega.no_periodo_checkout(inicio, fim)`: filtro pela **data real da conclusão**, com
|
||
**comparação naïve** (intervalo meio-aberto `>= início` e `< fim+1`), **sem conversão de fuso**
|
||
— igual à análise feita direto no banco.
|
||
- Aplicado em `contar_pagas`, elegibilidade do wizard, gráfico principal (`DATE(checkout)`) e na
|
||
exibição da data das entregas.
|
||
|
||
### 🧾 PDFs (Relatório e Extrato)
|
||
- **Relatório**: endereço completo com quebra de linha, larguras de coluna fixas, quebra de
|
||
página antes do RESUMO. **Rodapé via `canvas`** (na margem inferior — não sobrepõe mais o
|
||
conteúdo); margem inferior 72.
|
||
- **Extrato**: coluna **Unit.**, os 4 pilares sempre exibidos, **total de entregas + veículos**,
|
||
**selo de pagamento** (PAGO/PENDENTE) e **garantia de espaço para o QR Code**
|
||
(`start_new_page` se faltar espaço — o Prawn não pagina imagem sozinho).
|
||
|
||
### 🎨 UI / Identidade visual
|
||
- **Fonte base maior** para leitura: `html { font-size: 17px }` (desktop) e `18px` (mobile).
|
||
- **Contraste** elevado no dashboard (`text-gray-500/600` → `text-gray-400`).
|
||
- **Painel do motorista** redesenhado mobile-first (fontes maiores, botões full-width, alvos ≥48px).
|
||
- **Favicon** (van laranja): `public/favicon.png`.
|
||
- **Logo da Reem** na barra (sidebar + topo mobile): `public/logo-reem.png`.
|
||
- **Animação de abertura (splash) após login**: vídeo `public/login-animacao.{webm,mp4}` (WebM +
|
||
MP4 fallback), exibido **uma vez** via flag de sessão (`session[:mostrar_splash]`), com
|
||
`mix-blend-mode: screen` para "derrubar" o fundo preto do vídeo.
|
||
- `rack-mini-profiler` desativado em `config/initializers/rack_mini_profiler.rb`.
|
||
|
||
### 📌 Padrões a seguir (novos)
|
||
- **Tabelas externas / read-only**: acesso só por SELECT; nome de tabela em SQL **sempre** via
|
||
whitelist + `quote_table_name` (ver `Operacao`).
|
||
- **Datas financeiras**: usar **`checkout`** (`no_periodo_checkout`), comparação naïve sem fuso.
|
||
- **Gráficos**: Chart.js via CDN, padrão **IIFE** + `Chart.getChart(ctx)?.destroy()` (evita
|
||
"Canvas already in use" com Turbo).
|
||
- **Badges**: `badge_status` (consolidação) e `badge_pagamento` (pagamento).
|
||
- **Assets estáticos** (favicon, logo, vídeos) ficam em `public/` e são referenciados por caminho
|
||
absoluto (`/arquivo.ext`).
|
||
|
||
### Migrations adicionadas hoje
|
||
```
|
||
20260617000001_add_pagamento_to_consolidacao_motoristas.rb
|
||
20260617000002_add_operacoes_to_consolidacoes.rb
|
||
```
|
||
Aplicar com: `docker-compose exec app bundle exec rails db:migrate`
|
||
|
||
</details>
|
||
|
||
---
|
||
|
||
<details>
|
||
<summary><strong>🔧 Correções — Extrato PDF (18/06/2026)</strong></summary>
|
||
|
||
### 🔴 Bugs corrigidos
|
||
- **500 ao gerar extrato** (`gerar_pdf_extrato`): `Prawn::Errors::IncompatibleStringEncoding`
|
||
(`Encoding::UndefinedConversionError`). Os PDFs usavam a fonte **embutida** do Prawn
|
||
(Helvetica/AFM), que só aceita o charset **Windows-1252** — o caractere **`✓`** do selo
|
||
"✓ PAGO" não existe nesse charset e derrubava a geração.
|
||
✅ **Corrigido:** `BasePdf` agora registra a família **DejaVu Sans (TTF)** e a define como
|
||
fonte padrão (`registrar_fonte_utf8`), com suporte total a **UTF-8**. Vale para todos os PDFs
|
||
(extrato e relatório) — qualquer caractere Unicode passa a funcionar.
|
||
- Fontes versionadas em `app/assets/fonts/DejaVuSans.ttf` e `DejaVuSans-Bold.ttf`
|
||
(a imagem Docker `ruby:3.2.2-slim` não traz fontes do sistema; o `COPY . .` do Dockerfile e
|
||
o volume `.:/app` garantem que estejam disponíveis em runtime).
|
||
- **Assinaturas do extrato caíam na 2ª página:** após o QR Code o cursor ficava abaixo do
|
||
limite de quebra (`start_new_page if cursor < 120`), empurrando o bloco de assinaturas para
|
||
uma página nova. ✅ **Corrigido:** espaçamentos compactados com segurança (padding das
|
||
tabelas `[4,8]`, QR Code menor) + folga antes das assinaturas reduzida (`BasePdf#assinaturas`,
|
||
60 → 36, limite de quebra 120 → 100) — **o extrato agora cabe em uma única página**.
|
||
- **QR Code sobrepondo / sumindo:** o posicionamento por `move_up`/`move_down` era frágil e,
|
||
com a fonte nova, fazia o QR se sobrepor ao texto/assinaturas. ✅ **Corrigido:**
|
||
`desenhar_qrcode` agora desenha a imagem com **posição absoluta (`at:`)** — que não move o
|
||
cursor — e avança o cursor manualmente pela altura da imagem. Também garantido que cada caixa
|
||
(selo de pagamento, total) tenha folga **≥ a própria altura** para não sobrepor o conteúdo
|
||
seguinte.
|
||
|
||
- **QR Code não aparecia para alguns motoristas:** o QR aponta para o painel do motorista
|
||
(`/motorista/acesso/:token`), que **exige uma conta de usuário** (`User` motorista com
|
||
`login_token`). Sem conta correspondente, `user_motorista` ficava `nil` e a seção do QR era
|
||
**omitida silenciosamente**. Duas causas tratadas:
|
||
- **Nome não batia** (espaços/maiúsculas) entre `consolidacao_motoristas.motorista_nome`
|
||
(dado externo) e `users.nome` (digitado no admin). ✅ `ConsolidacoesController#buscar_user_motorista`
|
||
agora tenta o match exato e, se falhar, compara nomes **normalizados** (sem espaços
|
||
extras/duplos, case-insensitive).
|
||
- **Motorista sem conta:** ✅ em vez de omitir, o extrato agora desenha um **aviso**
|
||
("Cadastro de acesso ainda não disponível… solicite ao administrador") no lugar do QR
|
||
(`ExtratoPdf#desenhar_aviso_sem_acesso`). O QR só existe para motoristas **com cadastro**
|
||
no site — comportamento esperado, já que sem conta não há painel para acessar.
|
||
|
||
### 📌 Padrão (novo)
|
||
- **PDFs com Prawn**: usar **sempre** a fonte TTF registrada no `BasePdf` (DejaVu, UTF-8). Nunca
|
||
contar com a fonte embutida — ela quebra em qualquer caractere fora do Windows-1252.
|
||
|
||
</details>
|
||
|
||
---
|
||
|
||
<details>
|
||
<summary><strong>🆕 Atualização 19/06/2026 — Apontamento manual / Nota avulsa</strong></summary>
|
||
|
||
Permite incluir numa consolidação uma **entrega que chegou por fora** (ex.: nota que entrou
|
||
depois e não está no banco mensal das operações). O usuário digita o **número da nota (NF =
|
||
`reference_id`)**, o sistema busca os dados direto na `db_reem_simplerout_2026` (atualizada a cada
|
||
hora pela SimpleRoute) e adiciona a entrega ao fechamento do motorista.
|
||
|
||
> A tabela externa continua **read-only** — o apontamento só **lê** dela (`Entrega.por_nf`,
|
||
> sem filtro de período, restrito à conta Gade via `da_conta_gade`).
|
||
|
||
### 🆕 Funcionalidades
|
||
- **Apontamento dentro de uma consolidação existente** (wizard Passo 1): botão **"➕ Apontamento
|
||
manual"** → modal busca a NF → confere os dados (motorista/veículo/data) → escolhe **um ou
|
||
vários pilares** (Normal/Retirada/Bônus/Desconto) → adiciona.
|
||
- **"Nota avulsa"** (tela de Consolidações): botão **"➕ Nota avulsa"** → busca a NF → ao
|
||
prosseguir **cria já a consolidação** (nome `Avulsa nf<NF>`, período = data da entrega → fim do
|
||
mês, motorista vindo do `driver` da entrega) com o apontamento, e abre a tela da consolidação.
|
||
- **Múltiplas classificações** por apontamento: cria um pilar (`ConsolidacaoEntrega`) por tipo
|
||
escolhido, somando os valores (mesmo modelo multi-pilar das entregas normais).
|
||
- **Motorista automático**: vem do `driver` da entrega; se ainda não estiver na consolidação, é
|
||
incluído.
|
||
- **Gestão na tela da consolidação (show)**: seção **"➕ Apontamentos manuais"** lista NF,
|
||
motorista, pilares e valor, com botão de **remover** (a nota inteira). Na revisão (Passo 3) os
|
||
apontamentos aparecem com o selo **"➕ Apontamento"** e botão de remover (por pilar).
|
||
|
||
### ⚙️ Como funciona (técnico)
|
||
- Coluna **`manual` (boolean, default false)** em `consolidacao_entregas` distingue apontamento de
|
||
entrega elegível do período. `Consolidacao#classificadas_count` ignora os `manual: true` para
|
||
**não estourar a barra de progresso** nem o gate de finalização (`>= 100%`).
|
||
- Lógica centralizada em **`Consolidacao#adicionar_apontamento!(entrega, tipos, user)`** e
|
||
**`Consolidacao#recalcular_motorista!`** — reusadas pelos dois fluxos (wizard e avulsa). Preço por
|
||
pilar via `ConsolidacaoEntrega.valor_para(tipo)` (fonte única). Dados da entrega via
|
||
`Entrega#resumo_apontamento`.
|
||
- Rotas novas:
|
||
- `consolidacoes` (coleção): `GET buscar_nf` (JSON), `POST avulsa`.
|
||
- `consolidacao_entregas` (coleção): `GET buscar_nf`, `POST apontar`, `DELETE remover_apontamento`
|
||
(aceita `?id=` para um pilar ou `?tracking_id=&motorista=` para a nota toda).
|
||
- Stimulus: `apontamento_controller.js` (modal do wizard) e `nota_avulsa_controller.js` (modal da
|
||
index). ⚠️ Identificador do Stimulus segue o nome do arquivo em **dash-case**: `nota_avulsa_controller.js`
|
||
→ `data-controller="nota-avulsa"` / `data-action="nota-avulsa#..."` (e o elemento da ação precisa
|
||
estar **dentro** da `div` do controller).
|
||
|
||
### 🆕 Migration adicionada (rodar `db:migrate`)
|
||
```
|
||
20260619000001_add_manual_to_consolidacao_entregas.rb
|
||
```
|
||
|
||
```bash
|
||
docker-compose exec app bundle exec rails db:migrate
|
||
```
|
||
|
||
### 🧪 Testes
|
||
- `spec/models/consolidacao_spec.rb`: `classificadas_count` ignora apontamentos manuais e
|
||
`adicionar_apontamento!` (multi-tipo, idempotente, inclui o motorista e soma o valor).
|
||
|
||
</details>
|
||
|
||
---
|
||
|
||
<details>
|
||
<summary><strong>🆕 Atualização 22/06/2026 — Pilar Extraordinária, Perfil Externo, Falhadas, Relatórios e Segurança QR</strong></summary>
|
||
|
||
### 🆕 Funcionalidades
|
||
- **Pilar "Entrega Extraordinária"** — preço coringa para entregas fora do planejamento.
|
||
- Novo valor `extraordinaria` no `enum tipo` de `ConsolidacaoEntrega` (soma positiva, como Bônus). Sem migration de enum (coluna `integer`).
|
||
- Aparece em **todas** as telas dos pilares: wizard (passo 1), validação, revisão, nota avulsa, extrato, PDFs e composição do dashboard.
|
||
- **Preço padrão configurável** em *Configurações* (`preco_extraordinaria`) **e** **valor customizável por entrega**: ao marcar "Extra" no wizard abre um prompt já preenchido com o padrão (`valor_classificacao` no controller + `pedirValor()` no Stimulus).
|
||
- **Card "Consolidado / Pago"** no topo do dashboard (total consolidado + total já pago do período; reusa `@fin_custo_total` / `@fin_pago`).
|
||
- **Entregas falhadas no dashboard** — o card "Total Entregas" agora separa `entregues · pendentes · falhadas`.
|
||
- Novo `Entrega::STATUS_FALHA` + scope `falhadas`; o scope `pendentes` foi ajustado para não sobrepor (nem `completed` nem falha).
|
||
- **Relatórios financeiros em PDF (admin):**
|
||
- Por **consolidação** — `Pdf::RelatorioFinanceiroConsolidacaoPdf` (botão "📊 Relatório financeiro" na tela da consolidação).
|
||
- Por **período** — `Pdf::RelatorioFinanceiroPeriodoPdf` (botão "🖨️ Relatório" no dashboard, respeita o filtro de período/operação).
|
||
- **Novo perfil de usuário: "Externo (só dashboard)"** (`role: externo`).
|
||
- Login normal por e-mail/senha; enxerga **apenas o dashboard**. Bloqueado nas demais áreas pelas policies Pundit (`pode_consolidar?`/admin). Ideal para enviar a outras gerências visualizarem os indicadores.
|
||
- **Filtro de data re-estilizado** — calendário no tema escuro (flatpickr dark + acento laranja) e atalhos rápidos: **Hoje / 7 dias / Este mês / 30 dias**.
|
||
|
||
### 🔴 Bugs corrigidos
|
||
- **Botão "Arquivar"** não funcionava em consolidação zerada / rascunho. `Consolidacao#arquivar!` e `#reativar!` passaram a usar `update_columns` (pulam validações e o callback de recálculo) — arquivar é soft-delete administrativo e deve sempre funcionar. `turbo_confirm` movido para o `<form>` (mais confiável).
|
||
- **Caixa do filtro de data** mostrava uma data solta em vez do período. Causa: o flatpickr recebia datas em ISO (`YYYY-MM-DD`) com `dateFormat: 'd/m/Y'`. Corrigido passando objetos `Date` + separador `" até "`; seleção de **um único dia** agora aplica; re-inicialização em `turbo:load`.
|
||
- **Moeda sem separador de milhar** — padronizado **`R$ 1.234,56`** (milhar `.`, decimais `,`) em: helper `moeda` (`number_to_currency`), PDFs (`base_pdf`), `Configuracao#valor_formatado`, mensagens do `NotificacaoService` e tooltips/JS do dashboard e do wizard.
|
||
- **R$ "quebrado" no painel do motorista** (celular) — os valores grandes quebravam "R$" e o número em linhas separadas. Adicionado `whitespace-nowrap` + fonte responsiva (`text-4xl sm:text-5xl`) nos cards.
|
||
|
||
### 🔐 Segurança
|
||
- **QR Code do extrato não loga mais direto.** Antes, ler o QR (`/motorista/acesso/:token`) autenticava o motorista sem PIN. Agora o QR apenas **identifica** o motorista e leva à tela de PIN; ele precisa digitar o **PIN de 4 dígitos** para entrar — e o PIN tem que ser **do mesmo motorista do QR** (impede usar o QR de outra pessoa). A tela saúda pelo nome quando vem do QR.
|
||
|
||
### 🆕 Migration adicionada (rodar `db:migrate`)
|
||
```bash
|
||
docker-compose exec app bundle exec rails db:migrate
|
||
```
|
||
- `20260622000001_add_preco_extraordinaria_configuracao` — cria a config `preco_extraordinaria` (padrão `R$ 25,00`) em bancos já existentes (idempotente; não sobrescreve valor já definido).
|
||
|
||
### ✅ Confirmado (25/06/2026)
|
||
- O status de falha gravado pelo SimpleRoute é exatamente **`failed`** — `Entrega::STATUS_FALHA = %w[failed]` está correto e dispensa ajuste.
|
||
```bash
|
||
docker-compose exec app bundle exec rails runner "puts Entrega.da_conta_gade.distinct.pluck(:status).inspect"
|
||
```
|
||
|
||
</details>
|
||
|
||
---
|
||
|
||
<details>
|
||
<summary><strong>🆕 Atualização 25/06/2026 — Consolidação inclui entregas de insucesso (motorista foi ao local)</strong></summary>
|
||
|
||
A consolidação só considerava entregas **concluídas com sucesso**. Como o motorista
|
||
**se desloca até o local mesmo nas entregas que falham (insucesso)**, essas precisam
|
||
**constar na consolidação** para serem classificadas e remuneradas conforme o caso.
|
||
|
||
### 🆕 Funcionalidades
|
||
- **Novo conjunto elegível "atendidas"** — entregas em que o motorista foi ao local:
|
||
status **`completed` (sucesso) OU `failed` (insucesso)**, sempre com **checkout** registrado.
|
||
Substitui o antigo critério de só `.pagas` (`completed`) na consolidação.
|
||
- **Status visível em cada entrega** na tela de validação (Passo 2): selo **✓ Entregue** (verde)
|
||
ou **⚠️ Insucesso** (vermelho), com o card de insucesso destacado em **borda vermelha** —
|
||
facilita visualmente identificar o que classificar.
|
||
- **Insucesso entra para classificação manual**, não com valor automático: o admin escolhe o
|
||
pilar/valor (Normal, Retirada, Bônus, Desconto, Extraordinária) — o motorista foi ao local,
|
||
então a entrega aparece na lista, na contagem e na barra de progresso.
|
||
|
||
### ⚙️ Como funciona (técnico)
|
||
- `Entrega::STATUS_ATENDIDO = (%w[completed] + STATUS_FALHA)` + scope **`atendidas`**
|
||
(`where(status: STATUS_ATENDIDO).com_checkout`). Novo helper de instância `Entrega#falhada?`.
|
||
- `Entrega.contar_pagas` → renomeado para **`Entrega.contar_atendidas`** (usa o scope `atendidas`).
|
||
- `Consolidacao#entregas_elegiveis_count` passa a contar **atendidas** — reflete no wizard, na
|
||
barra de progresso e no **gate de finalização** (`todas_entregas_classificadas?`): agora as
|
||
entregas de insucesso também precisam ser classificadas antes de finalizar.
|
||
- `ConsolidacaoEntregasController`: a lista do Passo 2 (`validar`) e o guard de elegibilidade
|
||
(`tracking_ids_elegiveis`) usam `Entrega.atendidas`.
|
||
- **Não afeta o lado financeiro:** dashboard, painel do motorista, job de 1h e `HistoricoEstimado`
|
||
continuam usando `.pagas` (só `completed`) — "atendidas" é exclusivo da elegibilidade da
|
||
consolidação.
|
||
|
||
### 📂 Arquivos alterados
|
||
```
|
||
app/models/entrega.rb # STATUS_ATENDIDO, scope :atendidas, contar_atendidas, falhada?
|
||
app/models/consolidacao.rb # entregas_elegiveis_count usa contar_atendidas
|
||
app/controllers/consolidacao_entregas_controller.rb # validar + tracking_ids_elegiveis usam :atendidas
|
||
app/views/consolidacao_entregas/validar.html.erb # selo de status por entrega + destaque do insucesso
|
||
```
|
||
|
||
> Sem migration — a mudança é apenas de leitura/regra de negócio sobre a tabela read-only
|
||
> `db_reem_simplerout_2026`.
|
||
|
||
</details>
|
||
|
||
---
|
||
|
||
<details>
|
||
<summary><strong>🆕 Atualização 25/06/2026 — Apontamento 100% manual + Arquivar motorista individualmente</strong></summary>
|
||
|
||
Dois recursos na consolidação:
|
||
|
||
### 1. Apontamento 100% manual (tela de Validar — Passo 2)
|
||
Caso real: o motorista **A** foi até o local mas **não** entregou; no rastreio que alimenta o
|
||
banco, a NF ficou registrada sob o motorista **B** (que de fato entregou). Logo, a busca por NF
|
||
nunca encontra o A — mas o A também recebe, pois foi ao local. O apontamento manual antigo
|
||
(busca a NF na base e puxa `entrega.driver`) não serve.
|
||
- **Novo lançamento 100% manual**, sem consultar a base de rastreio, atribuível a **qualquer
|
||
motorista** (seletor com os motoristas da consolidação, ou digita um novo), com **NF (texto
|
||
livre)**, **observação/local** e **pilar(es)** (+ valor da Extraordinária).
|
||
- Botão **"➕ Apontamento 100% manual"** na tela de Validar. Soma ao valor do motorista, mas
|
||
**não** conta no gate `X/Y classificadas` (é `manual: true`, igual aos apontamentos por NF).
|
||
- A NF digitada aparece na Revisão (Passo 3), na tela da consolidação e no **Relatório PDF** do
|
||
motorista (fallback `nf_manual`/`obs_manual` quando não há `Entrega` para consultar).
|
||
|
||
### 2. Arquivar motorista individualmente (reversível)
|
||
Numa consolidação em massa, dá para **arquivar** um motorista específico no Passo 1 (botão 🗄️
|
||
por card) — ele **sai dos totais, do pagamento e do gate**, mas os dados ficam guardados. Seção
|
||
**"🗄️ Motoristas arquivados"** no fim do wizard com **"♻️ Restaurar"**.
|
||
- **Não** arquiva motorista **já pago** (estorne antes) — bloqueado com alerta.
|
||
- Útil para finalizar a consolidação ignorando um motorista que não deveria estar nela / sem
|
||
entregas, sem perder o registro.
|
||
|
||
### ⚙️ Como funciona (técnico)
|
||
- **Migrations** (rodar `db:migrate`):
|
||
- `20260625000001_add_arquivamento_to_consolidacao_motoristas` — `arquivado_em` / `arquivado_por`.
|
||
- `20260625000002_add_manual_fields_to_consolidacao_entregas` — `nf_manual` / `obs_manual`.
|
||
- `Consolidacao#adicionar_apontamento_manual!(motorista:, tipos:, user:, nf:, obs:, valor_extra:)`
|
||
— gera `tracking_id` sintético (`"MAN-<uuid>"`) compartilhado pelos pilares (agrupa como UMA
|
||
entrega); reusa `recalcular_motorista!`. Action `apontar_manual` + rota + Stimulus
|
||
`apontamento_manual_controller.js`.
|
||
- `ConsolidacaoMotorista`: scopes `ativos`/`arquivados`, `arquivar!`/`desarquivar!`, `arquivado?`.
|
||
Todo o "conjunto vigente" passou a usar **`.ativos`** (totais, pagamento, gate, notificações,
|
||
PDFs financeiros, painel do motorista e filtros de pagamento do dashboard) — o arquivado não
|
||
entra em nenhum agregado. Actions `arquivar_motorista`/`desarquivar_motorista` (autorização
|
||
`update?`; bloqueiam motorista já pago).
|
||
|
||
### 📂 Principais arquivos
|
||
```
|
||
db/migrate/20260625000001_add_arquivamento_to_consolidacao_motoristas.rb
|
||
db/migrate/20260625000002_add_manual_fields_to_consolidacao_entregas.rb
|
||
app/models/consolidacao.rb · consolidacao_motorista.rb
|
||
app/controllers/consolidacoes_controller.rb · consolidacao_entregas_controller.rb
|
||
app/controllers/dashboard_controller.rb · motorista/dashboard_controller.rb
|
||
app/javascript/controllers/apontamento_manual_controller.js (novo)
|
||
app/views/consolidacao_entregas/validar.html.erb · revisar.html.erb
|
||
app/views/consolidacoes/wizard.html.erb · show.html.erb
|
||
app/services/notificacao_service.rb · pdf/relatorio_motorista_pdf.rb · pdf/relatorio_financeiro_consolidacao_pdf.rb
|
||
config/routes.rb
|
||
spec/models/consolidacao_spec.rb · consolidacao_motorista_spec.rb
|
||
```
|
||
|
||
```bash
|
||
docker-compose exec app bundle exec rails db:migrate
|
||
docker-compose exec app bundle exec rspec
|
||
```
|
||
|
||
</details>
|
||
|
||
---
|
||
|
||
<details>
|
||
<summary><strong>🆕 Atualização 25/06/2026 — Fechar por veículo na tela de Validar</strong></summary>
|
||
|
||
Um motorista pode usar **vários veículos** no período. A tela de Validar (Passo 2) mostrava tudo
|
||
junto e o **"Marcar todos como Normal"** marcava **todas** as entregas do motorista. Agora dá
|
||
para **fechar cada carro individualmente**.
|
||
|
||
### 🆕 Funcionalidades
|
||
- **Filtro por veículo** (chips "🚗 Fechar por veículo") na tela de Validar — só aparece quando o
|
||
motorista usou **mais de um veículo**. Clicar num carro filtra a lista para aquele veículo; a
|
||
**barra de progresso** e o **"Marcar todos"/seleção** passam a valer **só para o carro
|
||
selecionado**. Há um chip **"Todos"** para a visão completa.
|
||
- **Status "fechado" por carro**: cada chip mostra `classificadas/elegíveis` e fica **verde com ✓**
|
||
quando o carro está 100% classificado — atualiza em tempo real (sem recarregar) ao classificar.
|
||
- A **finalização continua por motorista** (quando todos os carros de todos os motoristas estiverem
|
||
prontos) — o gate não mudou.
|
||
|
||
### ⚙️ Como funciona (técnico)
|
||
- `ConsolidacaoEntregasController#veiculos_status(motorista)` → `[{ vehicle, eligible,
|
||
classificadas, fechado }]` (2 queries: elegíveis com a coluna do veículo + classificadas
|
||
não-manuais). Reusa o scope `Entrega.da_veiculo`. Grupo "sem veículo" via sentinela `SEM_VEICULO`.
|
||
- `validar` aceita `params[:vehicle]`: recorta `@entregas` e, quando há carro selecionado, os totais
|
||
da barra (`@total_entregas`/`@classificadas`) vêm do `veiculos_status` daquele carro.
|
||
- `tracking_ids_elegiveis(motorista, vehicle = nil)` e `classificar_em_massa` passam o veículo —
|
||
é o que faz **"Marcar todos" fechar só o carro**. `resumo_json(motorista, vehicle:)` devolve
|
||
`classificadas_escopo` (barra por carro) + `veiculos_status` (refresh dos chips).
|
||
- `validacao_controller.js`: value `veiculo`, target `chipVeiculo`, `vehicle` nos POSTs,
|
||
`atualizarChips()` (verde/✓ + contadores). Sem migration — só leitura + UI.
|
||
|
||
### 📂 Arquivos
|
||
```
|
||
app/controllers/consolidacao_entregas_controller.rb # veiculos_status, validar, tracking_ids_elegiveis, classificar_em_massa, resumo_json
|
||
app/views/consolidacao_entregas/validar.html.erb # chips por veículo + barra com escopo
|
||
app/javascript/controllers/validacao_controller.js # value veiculo, chips, classificadas_escopo
|
||
```
|
||
|
||
</details>
|
||
|
||
---
|
||
|
||
<details>
|
||
<summary><strong>🆕 Atualização 26/06/2026 — Entrega de termo, caixa de ferramentas, gestão de motoristas e UX da Validação</strong></summary>
|
||
|
||
Pacote de implantação focado em **agilizar a precificação** e **dar feedback direto** ao usuário no
|
||
fluxo de consolidação. Resumo das frentes:
|
||
|
||
### 1. 📄 Entrega de termo (entrega sem NF nem código de rastreio)
|
||
Lançamento de **termos** entregues por um motorista — não têm nota fiscal nem `tracking_id` para
|
||
atrelar, só **motorista + quantidade** a um **preço fixo configurável**.
|
||
- Novo pilar `tipo: termo` (enum 5, cor azul) e novo preço **"Entrega de Termo"** editável em
|
||
**Admin → Configurações** (entra automático no card, via `CHAVES_MOEDA`).
|
||
- Lançado como **uma linha por lote** com a coluna nova **`quantidade`**; `valor_aplicado` guarda o
|
||
total do lote (`quantidade × preço`), então `recalcular_motorista!` e todas as somas existentes
|
||
seguem corretas. Modal com **stepper − N +** (sem as setinhas nativas do `input number`).
|
||
- Aparece na lista de **Apontamentos manuais** (badge "Entrega de Termo ×N"), nos PDFs (extrato,
|
||
relatório do motorista e financeiro) e no painel de Resumo — sempre contando por **`quantidade`**
|
||
(relatórios passaram de `count` para `sum(:quantidade)`).
|
||
|
||
### 2. 🧰 Caixa de ferramentas (toolbox) — botões unificados
|
||
- **Passo 2 (Validar):** os botões "Registrar entrega manualmente" e "Entrega de termo" viraram um
|
||
único **"➕ Adicionar lançamento ▾"** com menu suspenso. (O antigo "Apontamento 100% manual" foi
|
||
renomeado para **"Registrar entrega manualmente"** / modal "Registro manual de entrega".)
|
||
- **Passo 1 (Wizard):** o "Apontamento manual" virou **"➕ Adicionar / Ferramentas ▾"** com
|
||
**Apontamento manual** + **Adicionar motorista**.
|
||
- Reusa um controller Stimulus mínimo `ferramentas_controller.js` (só o dropdown); os modais e
|
||
controllers existentes não foram reescritos.
|
||
|
||
### 3. 👷 Gestão de motoristas na consolidação
|
||
- **Adicionar motorista** a uma consolidação já criada (ou **restaurar** se estava arquivado). O
|
||
campo puxa os **motoristas previstos no período** (atendidas, respeitando filtros de veículo/
|
||
operação) com os **veículos que cada um usou** — lista clicável + datalist.
|
||
- **Excluir motorista arquivado** (definitivo, **só admin/gerente**) — apaga os lançamentos dele
|
||
(ligados por `motorista_nome`, sem cascade) e recalcula o total. Bloqueia se já pago. Arquivar
|
||
continua sendo o caminho reversível.
|
||
- O **"← Voltar"** do wizard passou a ir para o **resumo da própria consolidação** (não mais a lista).
|
||
|
||
### 4. 📊 Resumo e barra de progresso com lançamentos manuais
|
||
- Painel **"📊 Resumo"** (Passo 2) ganhou linha **Termo** e linha **"Apontamentos manuais: N"**.
|
||
- **Barra de progresso** com **2 segmentos**: laranja→verde (elegíveis classificadas) + **azul**
|
||
(lançamentos manuais), com texto "· +N manuais". O gate de finalização **não mudou**
|
||
(`classificadas_count` segue `manual: false`) — é feedback visual e entra no valor.
|
||
- "Manuais" conta como **entregas**: cada termo vale sua `quantidade`; cada apontamento manual vale
|
||
1 por nota (`tracking_id`).
|
||
|
||
### 5. 🧊 UX da tela de Validar
|
||
- **Cabeçalho congelado** (no desktop): header, barra de progresso, filtro por veículo, ações em
|
||
massa e o botão de lançamento ficam fixos; **só a lista de NFs rola** (layout flex-column com a
|
||
coluna da lista `overflow-y-auto`). Evitou-se `sticky`/`z-index` para não prender os modais
|
||
(`fixed z-50`) atrás da sidebar (`z-40`).
|
||
- **Ações em massa simplificadas**: removidos o botão gigante **"Marcar todos como Normal"** e o
|
||
checkbox **"Modo remover"**. Os botões de pilar em **"Aplicar nos selecionados"** agora são
|
||
**toggle em massa** — aplicam o pilar nos selecionados e, se **todos** já o têm, removem de todos.
|
||
|
||
### ⚙️ Como funciona (técnico)
|
||
- **Migrations** (rodar `db:migrate`):
|
||
- `20260626000001_add_quantidade_to_consolidacao_entregas` — `quantidade` (default 1).
|
||
- `20260626000002_add_preco_termo_configuracao` — cria a config `preco_termo` (idempotente).
|
||
- `Consolidacao#adicionar_termos!(motorista:, quantidade:, user:)` — uma linha `TERMO-<uuid>`,
|
||
`manual: true`. `Configuracao.preco_termo` + `ConsolidacaoEntrega.valor_para('termo')`.
|
||
- `ConsolidacoesController#adicionar_motorista` / `#excluir_motorista` (member) +
|
||
`motoristas_previstos_no_periodo`. `ConsolidacaoEntregasController#apontar_termo`, `@manuais`,
|
||
`contar_manuais`, `por_tipo` agora `sum(:quantidade)`.
|
||
- Stimulus novos: `apontamento_termo_controller.js`, `ferramentas_controller.js`,
|
||
`adicionar_motorista_controller.js`. `validacao_controller.js`: barra com 2 segmentos, linhas
|
||
Termo/Manuais e **toggle em massa** (`pilarAtivo`); removidos `marcarTodos`/`modoRemover`.
|
||
|
||
### 📂 Principais arquivos
|
||
```
|
||
db/migrate/20260626000001_add_quantidade_to_consolidacao_entregas.rb
|
||
db/migrate/20260626000002_add_preco_termo_configuracao.rb
|
||
app/models/configuracao.rb · consolidacao.rb · consolidacao_entrega.rb
|
||
app/controllers/consolidacoes_controller.rb · consolidacao_entregas_controller.rb
|
||
app/javascript/controllers/apontamento_termo_controller.js (novo)
|
||
app/javascript/controllers/ferramentas_controller.js (novo)
|
||
app/javascript/controllers/adicionar_motorista_controller.js (novo)
|
||
app/javascript/controllers/validacao_controller.js
|
||
app/views/consolidacoes/wizard.html.erb · show.html.erb
|
||
app/views/consolidacao_entregas/validar.html.erb · revisar.html.erb
|
||
app/services/pdf/extrato_pdf.rb · relatorio_motorista_pdf.rb · relatorio_financeiro_consolidacao_pdf.rb
|
||
config/routes.rb · db/seeds.rb
|
||
```
|
||
|
||
```bash
|
||
docker-compose exec app bundle exec rails db:migrate
|
||
```
|
||
|
||
</details>
|
||
|
||
<details>
|
||
<summary><strong>🔧 Atualização 29/06/2026 — Responsividade da tela de Validar (telas menores)</strong></summary>
|
||
|
||
Correção da **quebra de layout** dos cards de NF na tela **Passo 2 (Validar)** em telas pequenas e
|
||
em tablets, reportada nos prints `Imagens para correção/Imagem colada (16).png` e `(17).png`.
|
||
|
||
### 🔴 Bugs corrigidos
|
||
- **Botão "Extra" cortado no celular** *(print 16)* — a fileira dos 5 pilares
|
||
(`Normal · Retirada · Bônus · Desc. · Extra`) usava `flex gap-1.5` **sem `flex-wrap`**, então não
|
||
quebrava linha e o último botão estourava a borda do card. Agora os botões **quebram para a linha
|
||
de baixo** quando não cabem.
|
||
- **Botões sobrepondo o texto da nota no tablet** *(print 17, ~800px)* — o card virava layout em
|
||
linha já no breakpoint `md` (768px), **exatamente onde a sidebar de 256px passa a aparecer**
|
||
(`md:ml-64`), espremendo o conteúdo. Sem `flex-wrap`, os botões transbordavam por cima do
|
||
endereço/data/veículo. O ponto de virada foi movido para `lg` (1024px), onde há largura real.
|
||
|
||
### ⚙️ Como funciona (técnico)
|
||
- `app/views/consolidacao_entregas/validar.html.erb`:
|
||
- Card da entrega: `flex flex-col md:flex-row md:items-center` → **`flex flex-col lg:flex-row
|
||
lg:items-center`** (empilha no celular **e no tablet**; em linha só no desktop).
|
||
- Grupo de toggles: `flex gap-1.5` → **`flex flex-wrap gap-1.5 lg:shrink-0`** (`flex-wrap` impede
|
||
o transbordo; `lg:shrink-0` mantém os botões inteiros no desktop, deixando o texto da nota
|
||
truncar em vez de espremer os botões).
|
||
- Faixa crítica resolvida (768–1023px): antes acumulava *sidebar visível + layout em linha + botões
|
||
sem quebra*. Agora **< 1024px** o card fica empilhado com botões em `flex-wrap` e **≥ 1024px** vai
|
||
para linha com truncamento do texto.
|
||
- Sem migration e sem mudança de JS/controller — alteração **somente de classes Tailwind** na view.
|
||
|
||
</details>
|
||
|
||
<details open>
|
||
<summary><strong>🆕 Atualização 29/06/2026 — Dashboard de Operações (análise de entregas + mapa)</strong></summary>
|
||
|
||
Nova página **`/dashboard/operacoes`** (link **📈 Operações** no menu), separada do dashboard
|
||
financeiro, focada na **qualidade das entregas** por operação (UBS Norte, EMAD, …). Reproduz os
|
||
painéis das imagens de referência (`Imagens para implantação/`) e adiciona análise comparativa e mapa.
|
||
Acessível a todos menos motorista (`DashboardPolicy#operacoes?`).
|
||
|
||
### 🆕 Funcionalidades
|
||
- **3 modos na mesma página** (abas):
|
||
- **🏥 Operação** — uma operação por vez. **Sem filtro de data**: mostra o conjunto inteiro da
|
||
operação e o período exibido vem dos próprios dados (menor/maior `checkout`).
|
||
- **🌐 Global** — todas as operações agregadas, aí sim com **faixa de datas** (flatpickr + atalhos
|
||
Hoje/7d/Este mês/30d). A faixa de data **só vale no Global**.
|
||
- **⚖️ Comparar** — duas operações lado a lado com bloco **Comparativo** (tabela A | B | Δ com o
|
||
melhor valor de cada linha em verde + gráfico de barras agrupadas) e detalhe completo recolhível.
|
||
- **Painéis** (espelham as imagens 2 e 3): KPIs (Total / Sucesso / Recusas / Pendentes), donut
|
||
**Insucessos %**, **Índices de Falha** (por `observation`), **Por Status** (status_gade
|
||
RECORRENTE/NOVO), **NFs por Motorista**, **Entregas por STS/Unidade** (`contact_name`), **Entregas
|
||
por Dia** (barras empilhadas) e **Mapa**.
|
||
- **Cross-filter ao vivo (toggle)** — clicar em qualquer linha das tabelas (Motorista, Unidade,
|
||
Status, Observação) **refiltra todo o dashboard** por aquele valor; clicar de novo na linha ativa
|
||
**remove** o filtro. Chips removíveis + "Limpar tudo" no topo.
|
||
- **Mapa de entregas** com:
|
||
- alternância **🔥 Calor** (heatmap) ⇄ **📍 Pontos** (um balão por entrega na coordenada de
|
||
**check-out**);
|
||
- **camadas** 🌙 Escuro (padrão) / 🗺️ Claro / 🛰️ Satélite (todas grátis, sem chave);
|
||
- **tela cheia** (Fullscreen API);
|
||
- **popup** por entrega (NF, destinatário, endereço, unidade, motorista, data) com a **foto da
|
||
fachada** (coluna `foto_da_fachada`), aberta em tamanho cheio ao clicar.
|
||
- **Cores** dos gráficos na identidade da marca: laranja `#f97316` (completas) e vermelho `#ef4444`
|
||
(falhas); no comparativo A = laranja, B = azul.
|
||
|
||
### ⚙️ Como funciona (técnico)
|
||
- Núcleo de dados em **`app/services/analytics/operacao_metricas.rb`** (PORO). Monta um SQL que
|
||
**espelha a query de gestão**: CTE `ROW_NUMBER() OVER (PARTITION BY reference_id ORDER BY checkout
|
||
DESC)` sobre `db_reem_simplerout_2026` + `INNER JOIN` na tabela `gade_entregas_*` por
|
||
`reference_id::text = nota_fiscal`; agrega tudo em Ruby (visão `linhas` = registros após o
|
||
cross-filter).
|
||
- **Sem filtro de conta** (`account_id`): o INNER JOIN com a tabela da operação já restringe ao
|
||
cliente — replicar `da_conta_gade` zerava tudo quando `DB_EXISTING_ACCOUNT_ID` não batia.
|
||
- **Schema gade não-uniforme**: só `nota_fiscal` é garantida. Colunas opcionais (`status`,
|
||
`nome_completo`, `endereco_completo`) são detectadas por tabela (`conn.columns`) e viram `NULL`
|
||
quando não existem — assim o `UNION ALL` entre operações no Global não quebra
|
||
(era o erro `column g.status does not exist`).
|
||
- **Segurança**: nomes de tabela passam por `Operacao.sanitizar` + `quote_table_name`; a `foto_da_fachada`
|
||
é validada como URL `http(s)` antes de ir ao `<img>`, com escape de HTML no popup.
|
||
- **Performance do mapa**: marcadores criados **sob demanda** (só ao abrir "Pontos") e o popup é uma
|
||
**função** — a foto do S3 só é requisitada **ao clicar** no balão (nada é baixado no load). Limite
|
||
de 2000 marcadores.
|
||
- **Filtro de período** opcional no serviço (`inicio:`/`fim:`): aplicado só no Global.
|
||
|
||
### 📂 Arquivos
|
||
```
|
||
app/services/analytics/operacao_metricas.rb # NOVO — núcleo de dados/agregações
|
||
app/controllers/operacoes_dashboard_controller.rb # NOVO — modos + cross-filter
|
||
app/views/operacoes_dashboard/index.html.erb # NOVO — filtros, abas, charts, mapa (JS)
|
||
app/views/operacoes_dashboard/_painel.html.erb # NOVO — KPIs/donut/tabelas/gráfico
|
||
app/views/operacoes_dashboard/_comparativo.html.erb # NOVO — tabela A|B|Δ + barras agrupadas
|
||
app/views/operacoes_dashboard/_mapa.html.erb # NOVO — calor/pontos, camadas, tela cheia
|
||
app/views/operacoes_dashboard/_tabela_simples.html.erb # NOVO — tabela reutilizável + cross-filter
|
||
spec/services/analytics/operacao_metricas_spec.rb # NOVO — specs das agregações/cross-filter
|
||
spec/requests/operacoes_dashboard_spec.rb # NOVO — autorização por role + modos
|
||
config/routes.rb # rota get /dashboard/operacoes
|
||
app/policies/dashboard_policy.rb # operacoes? (todos menos motorista)
|
||
app/views/layouts/_navbar.html.erb # link "📈 Operações"
|
||
```
|
||
|
||
> **Sem migration** — tudo lê tabelas existentes (`db_reem_simplerout_2026` read-only e
|
||
> `gade_entregas_*`). Libs front via CDN (Chart.js, Leaflet + leaflet.heat, flatpickr).
|
||
> ⚠️ Em produção, **reiniciar o servidor** (Puma) após o deploy — o Rails cacheia classes/views.
|
||
|
||
</details>
|
||
|
||
<details open>
|
||
<summary><strong>🆕 Atualização 30/06/2026 — Mapa, dashboard clicável, validação por veículo e UX da sidebar</strong></summary>
|
||
|
||
Rodada de melhorias no **Dashboard de Operações**, na tela de **Validação do motorista**
|
||
(consolidação) e no **layout do site inteiro**.
|
||
|
||
### 🗺️ Mapa de Operações
|
||
- **Busca no mapa** — campo 🔍 que filtra os pontos por **NF, nome do paciente ou veículo**,
|
||
troca para a visão "Pontos", dá zoom nos resultados (busca client-side) e mostra contador /
|
||
"Nenhum resultado".
|
||
- **Mostra TODAS as entregas** — antes só `completed`; agora **sucesso + insucesso** (óbito e
|
||
demais pilares de falha), via `Entrega::STATUS_ATENDIDO`. Pino **laranja = sucesso**, **azul =
|
||
insucesso**; o popup mostra o status e o **motivo** (`observation`). A **foto da fachada** é
|
||
mantida para todos os casos.
|
||
- **Campo veículo** — coluna `vehicle` (de `Entrega`) adicionada à query, ao card e à busca.
|
||
- **Modo escuro mais detalhado e com mais contraste** — o tile escuro passou de CARTO `dark_all`
|
||
(minimalista) para **OpenStreetMap completo invertido** (`filter: invert(1) hue-rotate(180deg)
|
||
brightness(.95) contrast(1.05)` na classe `.tiles-escuro-contraste`): mesmo detalhe de ruas/nomes
|
||
do mapa claro, porém escuro e legível. Claro/Satélite ficam intactos.
|
||
|
||
### 📊 Dashboard de Operações (painéis)
|
||
- **Gráficos clicáveis (cross-filter)** — além das tabelas, agora o **donut "Insucessos %"**
|
||
(clique em Completas/Falhas → filtra por `status`) e as barras **"Entregas por Dia"** (clique →
|
||
filtra por **dia** + **resultado** da barra) refiltram **todo** o dashboard. Novos filtros
|
||
`f_resultado` (coluna `status`) e `f_data` (por data de checkout, tratado à parte no serviço via
|
||
`@data`). Chips removíveis + "Limpar tudo", igual às tabelas.
|
||
- **Falha em azul** — a cor de insucesso mudou de vermelho `#ef4444` para **azul `#3b82f6`** nos
|
||
KPIs e gráficos (mais contraste no tema escuro).
|
||
- **Hover nos cards de KPI** — ao passar o mouse o card destaca, o número aumenta e aparece o
|
||
**% do total** (no card Total, o detalhamento ✅/●/⏳).
|
||
- **Respiro no scroll das tabelas** — `pr-2` nos contêineres roláveis (Motoristas / STS) para a
|
||
barra de scroll não colar no texto.
|
||
|
||
### 🚚 Validação do motorista (consolidação)
|
||
- **Filtro de veículos multi-seleção** — antes "um carro de cada vez"; agora dá pra **somar 2+
|
||
veículos**. A lista, a barra de progresso, o **valor total (R$)** e a **contagem por pilar**
|
||
passam a refletir só os veículos selecionados. Chip selecionado fica **laranja** (prioridade
|
||
sobre o verde de "fechado") para a seleção ficar sempre visível.
|
||
- **Lupa de busca na listagem de notas** — campo 🔍 sticky no topo da lista que filtra os cards
|
||
por **NF, local, endereço ou veículo**; "Selecionar todos" passa a marcar **apenas os visíveis**
|
||
no filtro.
|
||
- **Barra fixa mais compacta** — paddings/margens reduzidos (`p-3 mb-3`) para melhor aproveitamento
|
||
em **monitores pequenos**.
|
||
- **Ocultar/mostrar o resumo lateral** — botão 📊 que esconde o resumo e dá **largura total** à
|
||
lista (`lg:col-span-3 → lg:col-span-4`).
|
||
|
||
### 🧭 Layout global (site inteiro)
|
||
- **Recolher/expandir a sidebar do menu principal** — botão flutuante de seta (desktop) que esconde
|
||
o menu e expande o conteúdo (`md:ml-64 → 0`). Estado **lembrado** entre páginas (`localStorage` +
|
||
classe `.sidebar-collapsed` no `<html>`, aplicada antes de pintar → sem "piscar").
|
||
- **Indicador discreto** — em repouso o botão é uma **alça fina laranja** sempre visível na borda;
|
||
ao chegar perto com o mouse (ou foco por teclado) ela **expande** no botão com a seta ◀/▶.
|
||
|
||
### 📂 Arquivos
|
||
```
|
||
app/services/analytics/operacao_metricas.rb # veículo, sucesso+insucesso, filtro por dia (@data)
|
||
app/controllers/operacoes_dashboard_controller.rb # CROSS f_resultado + filtro f_data
|
||
app/views/operacoes_dashboard/index.html.erb # busca/dark map, pinos, gráficos clicáveis, KPIs
|
||
app/views/operacoes_dashboard/_painel.html.erb # hover KPIs, cor azul, pr-2 no scroll
|
||
app/views/operacoes_dashboard/_mapa.html.erb # campo de busca no mapa
|
||
app/controllers/consolidacao_entregas_controller.rb # filtro multi-veículo + resumo por escopo
|
||
app/views/consolidacao_entregas/validar.html.erb # chips multi, lupa, barra compacta, toggle resumo
|
||
app/javascript/controllers/validacao_controller.js # veiculos[] , busca, toggleResumo
|
||
app/views/layouts/application.html.erb # CSS sidebar recolhível + script anti-flash
|
||
app/views/layouts/_navbar.html.erb # botão flutuante + alça indicadora
|
||
```
|
||
|
||
> **Sem migration** — segue lendo as tabelas existentes (`db_reem_simplerout_2026` read-only e
|
||
> `gade_entregas_*`). ⚠️ Em produção, **reiniciar o Puma** após o deploy (cache de classes/views).
|
||
|
||
</details>
|
||
|
||
---
|
||
|
||
<details>
|
||
<summary><strong>🔒 Segurança + auto-atualização do painel + cron (06/07/2026)</strong></summary>
|
||
|
||
Revisão de segurança do site em produção (via browser + inspeção de headers) e
|
||
melhorias no **Dashboard de Operações**. **Sem migration.**
|
||
|
||
### 🩸 Causa-raiz descoberta — produção em modo `development`
|
||
A página de erro 404 do site expunha o backtrace do Rails e havia o cookie
|
||
`__profilin` (rack-mini-profiler) — sinais de que o container roda em
|
||
**`RAILS_ENV=development`**. Isso é a origem de várias falhas abaixo (cookie de
|
||
sessão sem `Secure`, sem redirect HTTPS, sem CSP, páginas de exceção vazando
|
||
código).
|
||
|
||
> ⚠️ **Ação necessária no servidor** (o `.env` não está no repositório): definir
|
||
> `RAILS_ENV=production` e `FORCE_SSL=true` no `.env` e rebuildar. Ao migrar para
|
||
> `production`, validar o carregamento dos assets (Tailwind via CDN + importmap).
|
||
|
||
### 🔴 Correções de segurança
|
||
- **HTTPS obrigatório + cookie `Secure` (itens 1 e 2)** — `config.force_ssl` +
|
||
`assume_ssl` (este último para o TLS terminado no Cloudflare, evitando loop de
|
||
redirect). Em `application.rb` fica atrás de `ENV["FORCE_SSL"]=="true"`, então
|
||
**funciona mesmo enquanto o deploy roda fora do modo production**;
|
||
`production.rb` também já vem com `force_ssl = true`.
|
||
- **rack-mini-profiler desligado (item 3)** — no `Gemfile` passou a `require:
|
||
false`: a gem não monta mais o middleware (fim do cookie `__profilin` e da rota
|
||
`/mini-profiler-resources`), em qualquer ambiente.
|
||
- **Content Security Policy (item 5)** — nova política em
|
||
`config/initializers/content_security_policy.rb` com allowlist dos CDNs reais
|
||
(Tailwind, Chart.js, Flatpickr, Leaflet, Google Fonts, tiles ArcGIS/OSM).
|
||
Começa em **Report-Only** (só reporta violações, não bloqueia) para não quebrar
|
||
mapa/gráficos; instruções no topo do arquivo para ativar o bloqueio
|
||
(`report_only = false`) depois de validar no console.
|
||
- Erro de login genérico ("Invalid email or password.") e recuperação de senha
|
||
**não** revelam se o e-mail existe (sem enumeração de usuários) — **OK**, sem
|
||
mudança. Pendência conhecida: login por **PIN de 4 dígitos sem identificador** —
|
||
avaliar rate-limiting/rack-attack.
|
||
|
||
### 🔄 Painel de Operações atualiza sozinho (sem F5)
|
||
- Novo `app/javascript/controllers/auto_refresh_controller.js` — a cada **5 min**
|
||
(`data-auto-refresh-interval-value`, em ms) faz `Turbo.visit` na própria URL:
|
||
re-renderiza KPIs/gráficos/mapa **sem o flash do F5**, **preserva os filtros**
|
||
(que vivem na query string) e a **posição de rolagem**; pausa em aba oculta ou
|
||
quando há um campo em foco.
|
||
|
||
### 🕑 Horário da "última atualização" corrigido
|
||
- Antes mostrava `Time.now` — o **fuso do container (UTC)** e a **hora de
|
||
renderização** (por isso aparecia adiantado e sempre "agora"). Agora um helper
|
||
(`ultima_atualizacao_dados` / `ultima_atualizacao_label`) calcula o **último
|
||
slot real de sincronização** (a cada 30 min, das 08h às 18h) no fuso de
|
||
Brasília (`Time.current`).
|
||
|
||
### ⏰ Agendamento (whenever/cron) no Docker
|
||
- `config/schedule.rb` — job de histórico passou de `every 1.hour` para
|
||
**`*/30 8-18`** (a cada 30 min, 08h-18h), batendo com a cadência real.
|
||
- `Dockerfile` — instala o pacote **`cron`**.
|
||
- `docker-compose.yml` — o boot agora roda, em ordem: `db:prepare` →
|
||
**`whenever --update-crontab`** (grava os jobs) → **`cron`** (sobe o daemon) →
|
||
`rails s`. As variáveis do banco chegam ao job via **dotenv** quando o rake
|
||
carrega o Rails.
|
||
|
||
### 📂 Arquivos
|
||
```
|
||
config/application.rb # gate FORCE_SSL → force_ssl + assume_ssl
|
||
config/environments/production.rb # force_ssl = true (+ assume_ssl)
|
||
config/initializers/content_security_policy.rb # CSP (report-only) — NOVO
|
||
config/initializers/rack_mini_profiler.rb # comentário atualizado
|
||
Gemfile # rack-mini-profiler, require: false
|
||
.env.example # RAILS_ENV=production + FORCE_SSL=true
|
||
app/helpers/application_helper.rb # ultima_atualizacao_dados / _label
|
||
app/javascript/controllers/auto_refresh_controller.js # auto-refresh do painel — NOVO
|
||
app/views/operacoes_dashboard/index.html.erb # data-controller auto-refresh + horário
|
||
config/schedule.rb # cron */30 8-18
|
||
lib/tasks/historico.rake # desc atualizada
|
||
Dockerfile # instala cron
|
||
docker-compose.yml # whenever --update-crontab + cron no boot
|
||
```
|
||
|
||
### ▶️ Deploy
|
||
```bash
|
||
# 1. No servidor, ajustar o .env:
|
||
# RAILS_ENV=production
|
||
# FORCE_SSL=true
|
||
# 2. Rebuildar e subir (já roda whenever --update-crontab + cron no boot):
|
||
docker compose up -d --build
|
||
# 3. Validar a CSP no navegador (F12 → console, sem violações) e então trocar
|
||
# config/initializers/content_security_policy.rb para report_only = false.
|
||
```
|
||
|
||
> **Sem migration.** ⚠️ Em produção, **reiniciar o Puma** após o deploy.
|
||
|
||
</details>
|
||
|
||
---
|
||
|
||
<details>
|
||
<summary><strong>✏️ Atualização 08/07/2026 — Editar Lançamento do SimpliRoute (correção pelo ADM)</strong></summary>
|
||
|
||
Nova tela **só para ADM** que permite **corrigir um lançamento** de entrega direto na
|
||
**API do SimpliRoute** (ex.: uma entrega marcada como "completa" que na verdade foi
|
||
**óbito**). Até então a única forma era entrar manualmente no SimpliRoute. **Sem migration.**
|
||
|
||
### 🆕 Funcionalidades
|
||
- **Menu → Administração → "✏️ Editar Lançamento"** (visível só para `admin`).
|
||
- **Busca por NF** → o sistema localiza a entrega e mostra o estado atual.
|
||
- **Campos editáveis:** `status` (completa/falha/pendente/parcial/cancelada), **motivo**
|
||
(óbito, endereço não localizado, mudou-se, etc.), **comentário**, **observações** e,
|
||
no avançado, **data/hora + geolocalização** do checkout.
|
||
- **Auditoria** — toda alteração grava um `AuditoriaLog` (quem, quando, de/para).
|
||
- **Foto da fachada / assinatura** ficam para uma **fase 2** (exigem upload de imagem).
|
||
|
||
### ⚙️ Como funciona (técnico)
|
||
- A base de rastreio local (`db_reem_simplerout_2026`, model `Entrega`) é **read-only** e
|
||
sincroniza a partir do SimpliRoute. A correção **não** escreve nela — grava na **API**;
|
||
o painel/consolidações só refletem **na próxima sincronização** (a tela avisa isso).
|
||
- **Dois identificadores:** a visita tem `id` numérico (usado na URL de escrita) **e**
|
||
`tracking_id` (`SR...`, que é a PK do espelho). O espelho não guarda o id numérico, então
|
||
o serviço **resolve** listando as visitas da data (`planned_date`) e casando pelo
|
||
`tracking_id` (fallback: NF).
|
||
- **Motivo** = campo `checkout_observation`, que é o **UUID** de uma lista fixa de 14 motivos
|
||
(`GET /v1/routes/observations/`, todos `type=failed`). É esse UUID que popula a coluna
|
||
`observation` — **não** basta texto livre no comentário.
|
||
- A gravação usa **PATCH** (só os campos alterados), para **não** sobrescrever assinatura/geo
|
||
originais sem intenção. Token da API vem de **`SIMPLIROUTE_TOKEN`** (ENV, nunca no git).
|
||
|
||
### 🔧 Correção — botão estava no menu errado
|
||
- O botão foi adicionado, por engano, ao `app/views/layouts/_sidebar.html.erb`, que é um
|
||
**partial morto** (legado da fase 2/3, nunca renderizado). O menu **real** é o
|
||
**`_navbar.html.erb`** (renderizado pelo `application.html.erb`). O link foi movido para lá,
|
||
na seção Administração.
|
||
|
||
### 📂 Arquivos
|
||
```
|
||
config/initializers/simpli_route.rb # lê SIMPLIROUTE_TOKEN / BASE_URL — NOVO
|
||
app/services/simpli_route/client.rb # cliente Net::HTTP (observations, resolver_id, PATCH) — NOVO
|
||
app/controllers/admin/edicao_lancamentos_controller.rb # busca/atualiza + AuditoriaLog — NOVO
|
||
app/policies/edicao_lancamento_policy.rb # admin-only — NOVO
|
||
app/views/admin/edicao_lancamentos/show.html.erb # tela (busca NF + form) — NOVO
|
||
config/routes.rb # resource admin/edicao_lancamento
|
||
app/views/layouts/_navbar.html.erb # link "✏️ Editar Lançamento" (admin)
|
||
.env.example # SIMPLIROUTE_TOKEN + SIMPLIROUTE_BASE_URL
|
||
```
|
||
|
||
### ▶️ Deploy
|
||
```bash
|
||
# 1. No servidor, adicionar ao .env (pegue o token em SimpliRoute → Configurações → API):
|
||
# SIMPLIROUTE_TOKEN=...
|
||
# SIMPLIROUTE_BASE_URL=https://api.simpliroute.com
|
||
# 2. Rebuildar/reiniciar:
|
||
docker compose up -d --build
|
||
# 3. Logar como admin → Administração → "Editar Lançamento" → buscar uma NF e testar.
|
||
```
|
||
|
||
> **Sem migration.** ⚠️ Em produção, **reiniciar o Puma** após o deploy (cache de classes/views).
|
||
|
||
</details>
|
||
|
||
---
|
||
|
||
<details>
|
||
<summary><strong>🔭 Roadmap — Integrações futuras com a API SimpliRoute (08/07/2026)</strong></summary>
|
||
|
||
Mapeamento completo da API do SimpliRoute (documentation.simpliroute.com) cruzado com o fluxo
|
||
do sistema. **Nada aqui está implementado** — são os próximos passos priorizados. A base
|
||
técnica já existe (`SimpliRoute::Client`, `SimpliRoute::PlanilhaCarga`, token via ENV):
|
||
cada item vira uma fase própria quando for priorizado.
|
||
|
||
### 1. 🚀 Carga direta no SimpliRoute (substitui o baixar/importar planilha)
|
||
`POST /v1/routes/visits/` aceita criação **em lote**: um botão "Subir para o SimpliRoute"
|
||
criaria as visitas da operação vigente direto pela API, com as mesmas regras da planilha
|
||
(título `NF {nf} - {nome}`, janelas 08:00–18:00, lat/long do mês anterior, etc.).
|
||
> ⚠️ **Pré-requisito que segura esta fase: consolidar os ENDEREÇOS antes da subida.**
|
||
- Anti-duplicação: conferir `reference` + `planned_date` antes de criar (reexecutar não duplica).
|
||
- Desfazer: `POST /v1/bulk/delete/visits/` permite implementar um "remover carga".
|
||
|
||
### 2. 🧾 Relatório de comprovantes de entrega (POD)
|
||
`GET /v1/plans/visits/{visit_id}/detail/` traz **foto, assinatura, hora e GPS** de cada
|
||
entrega → gerar PDF por operação/mês (reuso do padrão Prawn em `app/services/pdf/`).
|
||
Valor: faturamento com STS/prefeitura e defesa em disputas ("não recebi").
|
||
|
||
### 3. ⚡ Webhooks — painel em tempo real
|
||
`POST /v1/addons/webhooks/` com eventos `visit_checkout`, `route_started/finished`,
|
||
`on_its_way` → endpoint público autenticado no app grava o evento e o painel atualiza
|
||
**na hora**, sem esperar a sync de 30 min. Exige atenção à segurança (assinatura,
|
||
idempotência) e URL pública estável.
|
||
|
||
### 4. 📱 Aviso ao paciente via WhatsApp
|
||
Evento `on_its_way` + ETA da API + Twilio já existente (`NotificacaoService`):
|
||
"seu medicamento saiu para entrega". Reduz insucesso por **RESPONSÁVEL AUSENTE**
|
||
(motivo real da lista de observations).
|
||
|
||
### 5. 📺 Monitor de rotas ao vivo
|
||
`GET /v1/plans/{date}/vehicles/` + visitas por rota → tela "Operação de hoje" com cada
|
||
veículo, % concluído e atrasos (complementa o dashboard, que olha o passado).
|
||
|
||
### 6. 👷 Sincronização de motoristas/veículos
|
||
`GET /v1/accounts/drivers/` e `GET /v1/routes/vehicles/` ↔ usuários motoristas do sistema
|
||
(hoje o vínculo é o nome digitado — sujeito a divergência).
|
||
|
||
### 7. 📊 Datamart e 🏷️ tags/skills
|
||
Export paginado de analytics para enriquecer dashboards; tags/skills para classificar
|
||
visitas por tipo de material.
|
||
|
||
</details>
|
||
|
||
---
|
||
|
||
<details>
|
||
<summary><strong>🆕 Atualização 13/07/2026 — Planilha da Operação (página + Excel do cliente), dashboard e correções</strong></summary>
|
||
|
||
### 📋 Planilha da Operação — página própria (`/dashboard/operacoes/planilha`)
|
||
Tabela **espelho da planilha da operação** (uma linha por NF) para pesquisar e acompanhar
|
||
ocorrências/status, acessada pelo botão **"📋 Planilha da operação"** no Dashboard de Operações
|
||
(o botão leva junto o contexto atual: operação/modo Global, período e cross-filters ativos).
|
||
- Colunas: Data, NF, Destinatário (endereço no tooltip), Unidade STS, Motorista, Veículo,
|
||
**Resultado** (badge Entregue/Falha/Pendente), **Ocorrência** (motivo do insucesso) e status
|
||
gade (RECORRENTE/NOVO). No modo Global aparece também a coluna Operação.
|
||
- **Busca textual server-side** (param `q`, qualquer campo) + **paginação server-side**
|
||
(param `pg`, 15/página, janela `« 1 … 7 8 9 … 42 »`) — aguenta as milhares de linhas do Global.
|
||
- Alternância 🏥 Operação / 🌐 Global, seletor de operação (auto-submit) e chips removíveis dos
|
||
filtros herdados. Reusa as linhas do `Analytics::OperacaoMetricas` (novo método `#buscar`) —
|
||
**nenhuma query nova**.
|
||
- Arquivos: rota `operacoes_planilha`, action `planilha` + `montar_tabela_espelho`
|
||
(`OperacoesDashboardController`), views `planilha.html.erb` + `_espelho.html.erb`.
|
||
|
||
### ⬇️ Download do Excel do cliente — planilha Entregas preenchida
|
||
Botão **"⬇️ Baixar Excel preenchido"** na página da planilha: gera o `.xlsx` no formato do modelo
|
||
**"Entregas SUDESTE MM.AAAA_FINAL"** com as **3 abas, 1:1** — automatiza o preenchimento que era
|
||
feito manualmente para entregar ao cliente.
|
||
- **RESUMO** — mês, Total Previsto/Realizado/Performance e os quadros **EMAD** e **UBS** por
|
||
coordenadoria (CRS fixas do modelo) com **motivos de "Não Entregue"** (Óbito, Responsável
|
||
Ausente, Endereço não localizado, Recusa, Paciente não reside, Outros) classificados a partir
|
||
da `observation` do rastreio; linhas Total e Performance %.
|
||
- **ENTREGAS** — colunas A–V da própria tabela `gade_entregas_*` (ordem física de importação =
|
||
ordem da planilha original) + **W–Z preenchidas pelo rastreio**: STATUS, ENTREGA (Sim/Não),
|
||
DATA OCORRÊNCIA (data real do checkout) e OCORRÊNCIA — último status de cada NF (mesma CTE
|
||
`ROW_NUMBER` dos painéis).
|
||
- **SimpliRoute** — dump cru do rastreio com as **46 colunas exatas** do export original
|
||
(todas as visitas das NFs da operação, incluindo repetidas).
|
||
- **Estilo idêntico ao modelo** (cores extraídas do `styles.xml` do próprio arquivo): cabeçalho
|
||
ENTREGAS azul `1155CC` (A–T) + vermelho `C00000` (U–Z), quadros do RESUMO em
|
||
`002060`/`0070C0`/`C00000`/`A5A5A5`, status verde/laranja/azul, títulos 16pt, performance em
|
||
itálico %, cabeçalhos mesclados. Validado offline com caxlsx + LibreOffice.
|
||
- **Links das fotos são hiperlinks clicáveis** (azul sublinhado): Nota Fiscal, Termo de
|
||
Recebimento, Foto da Fachada e Relatório de visita abrem o comprovante direto do Excel.
|
||
Qualquer célula que seja URL `http(s)` vira link (`#linkar_urls`).
|
||
- Arquivos: `Analytics::PlanilhaEntregas` (dados) + `Analytics::PlanilhaEntregasXlsx` (binário,
|
||
caxlsx), rota `operacoes_planilha_baixar`, action `baixar_planilha`.
|
||
|
||
#### Nomes de coluna — conferidos no banco real (22/07/2026)
|
||
Os nomes eram **deduzidos** e 9 cabeçalhos nunca casavam, saindo vazios em silêncio. Os nomes
|
||
reais foram conferidos com `Entrega.column_names` e estão como 1º candidato em
|
||
`RASTREIO_COLUNAS`; os chutes antigos continuam na lista como rede de segurança.
|
||
- Espelho usa o padrão do export pt-BR: `codrivers`, `responsible_person`, `advance`,
|
||
`start_of_time_window_1..2`, `end_of_time_window_1..2`, `required_skills`, `optional_skills`,
|
||
`comments`. Corrigidos — antes eram `copilots`, `receiver`, `early`, `window_start`, etc.
|
||
- Tabelas `gade_entregas_*` gravam a geo como **`lat`/`long`** (não `latitude`/`longitude`) —
|
||
as duas colunas da aba ENTREGAS saíam vazias. Ver `ALIAS_GADE`.
|
||
- **`Load 4` não existe** no espelho (só `load`, `load_2`, `load_3`) — sai vazia de propósito.
|
||
- Coluna **`protocolo_de_entrega`** existe no espelho e não estava em lugar nenhum: entrou como
|
||
última coluna (AU), **depois** do layout A–AT do modelo, para não deslocar nada do cliente.
|
||
- ⚠️ Vazias por **falta de dado na origem**, não por bug: `contact_phone`, `account_id`,
|
||
`account_name` (colunas existem no espelho e o sync nunca preenche — a API do SimpliRoute
|
||
TEM o valor, conferido na NF 79093) e `codrivers`/`required_skills`/`optional_skills`/
|
||
`contact_email`/`load_2`/`load_3` (vazios também no relatório baixado direto do SimpliRoute).
|
||
- ⚠️ Os 2 **gráficos embutidos** do RESUMO não são replicados. "Conferência Documentos" é
|
||
controle manual (sai "A iniciar").
|
||
|
||
#### 🔄 Número de série da base (aparelho) — job em background
|
||
A coluna AT da aba SimpliRoute é o único campo do modelo que o espelho **nunca** traz (0 de
|
||
~6.000 linhas em fev/mar/mai/jul/2026), e não dá para deduzir da `gade_entregas_*`: os seriais
|
||
de `num_serie_base` são de NFs **disjuntas** das que têm série no SimpliRoute (0 de 203 batem).
|
||
O dado só existe na API, em `extra_field_values` — o mesmo hash das fotos do motorista.
|
||
- `SincronizarSeriesAparelhoJob` varre a API por DATA (a API não filtra intervalo; cada dia são
|
||
~4 MB / ~9 s) em lotes de 6 threads e grava em **`series_aparelho`** (tabela nossa — o espelho
|
||
é read-only). Um dia que não responde vira aviso, não derruba a rodada.
|
||
- `Analytics::PlanilhaEntregas#completar_serie` completa **só** as linhas em que o espelho veio
|
||
vazio: se um dia o sync passar a preencher, o valor do espelho continua ganhando.
|
||
- Agendado em `config/schedule.rb` às **01h** (últimos 45 dias). Backfill de meses antigos:
|
||
`rake "simpli_route:series_operacao[gade_entregas_ubs_sudeste_jul_2026]"` ou
|
||
`rake "simpli_route:series_periodo[2026-07-01,2026-07-31]"`.
|
||
- Se a rodada varrer visitas e não achar **nenhuma** série, o log lista as chaves de
|
||
`extra_field_values` que vieram — é o sinal de que o campo mudou de nome e basta acrescentá-lo
|
||
em `CAMPOS_SERIE` (foi chute de nome de campo que causou o bug acima).
|
||
|
||
### 📊 Dashboard financeiro
|
||
- **Paginação nas tabelas de pagamentos** ("realizados" e "pendentes"): 10 linhas/página com
|
||
abas e contador ("1–10 de 47"), client-side (função genérica `paginarTabela`); some com ≤10 itens.
|
||
- **Novo gráfico "Pagamentos por dia"** abaixo da "Evolução do custo": barras verdes com o valor
|
||
pago por data de pagamento (`pago_em`) + linha tracejada do **acumulado** — o fluxo de caixa,
|
||
que não existia em nenhuma tela (`build_grafico_pagamentos`). Quando não há pagamento no
|
||
período mostra estado vazio (💸 + valor aguardando pagamento) em vez de gráfico zerado.
|
||
> Iterações descartadas no caminho: "Entregas por dia" (redundante — valor = qtd × preço fixo)
|
||
> e "Resultado por dia" (já existe na tela de Operações).
|
||
- O card dos gráficos virou `flex-col` e preenche toda a altura ao lado do ranking de motoristas
|
||
(sumiu o vazio embaixo da linha).
|
||
|
||
### 🔴 Correções
|
||
- **"Rodapé quebrado" no celular:** era uma **barra de rolagem horizontal** (laranja) colada no
|
||
rodapé — a legenda dos gráficos não quebrava linha e estourava ~37px a largura em telas
|
||
<~430px. ✅ Cabeçalhos/legendas dos gráficos com `flex-wrap` + **`overflow-x-hidden` no
|
||
`<body>`** (rede de segurança; tabelas largas continuam rolando nos próprios contêineres
|
||
`overflow-x-auto`).
|
||
|
||
### 📌 Padrões (reforçados)
|
||
- Tabelas externas sempre read-only, nome de tabela via whitelist + `quote_table_name`, colunas
|
||
em whitelist fixa com fallback NULL (padrão `OperacaoMetricas`/`PlanilhaCarga`).
|
||
- Excel com `caxlsx` (mesmo padrão da planilha de carga SimpliRoute): serviço de linhas +
|
||
serviço de binário + `send_data` no controller.
|
||
|
||
> **Sem migration.** Deploy normal; em produção reiniciar o Puma após o deploy.
|
||
|
||
</details>
|
||
|
||
---
|
||
|
||
<details>
|
||
<summary><strong>🆕 Atualização 15/07/2026 — Entrega de Termo Especial (novo card de preço + lançamento com dois tipos)</strong></summary>
|
||
|
||
O negócio passou a diferenciar **termo normal** e **termo especial**, cada um com seu preço.
|
||
O modal de lançamento agora permite informar **quantos termos de cada tipo** o motorista
|
||
entregou, num único envio.
|
||
|
||
### 🆕 Funcionalidades
|
||
- **Novo card de preço "📋 Entrega de Termo Especial"** em **Admin → Configurações**
|
||
(chave `preco_termo_especial`, moeda). Criado com **R$ 0,00** — definir o valor real no
|
||
card antes de usar. O card aparece automático (a view itera todas as configs).
|
||
- **Novo pilar `tipo: termo_especial`** (enum 6, cor **ciano**, label "Entrega de Termo
|
||
Especial") — mesmo comportamento do termo normal: lote numa linha (`quantidade` × preço),
|
||
`manual: true`, `tracking_id` sintético `TERMO-<uuid>`.
|
||
- **Modal "📄 Entrega de termo" com dois steppers** lado a lado — "📄 Termo Normal"
|
||
(inicia em 1) e "📋 Termo Especial" (inicia em 0), cada um exibindo seu preço unitário.
|
||
Um clique em "Adicionar termos ✓" lança os dois lotes de uma vez (exige ao menos 1 termo
|
||
no total; mínimo 0 em cada stepper).
|
||
- **Exibição em todas as telas**: linha "Termo Especial" no painel 📊 Resumo do Passo 2,
|
||
card próprio no Revisar, badge ciano "Entrega de Termo Especial ×N" nos apontamentos
|
||
manuais (show) e nos PDFs (extrato, relatório do motorista e financeiro — automático,
|
||
iteram `TIPO_CORES`).
|
||
|
||
### ⚙️ Como funciona (técnico)
|
||
- **Migration** (rodar `db:migrate`): `20260715000001_add_preco_termo_especial_configuracao`
|
||
— cria a config `preco_termo_especial` (idempotente) + seed correspondente.
|
||
- `Configuracao`: chave nova em `CHAVES`/`CHAVES_MOEDA`/`LABELS`/`ICONES`/`mapa_de_precos`
|
||
+ helper `preco_termo_especial`.
|
||
- `Consolidacao#adicionar_termos!` ganhou o parâmetro **`tipo:`** (default `'termo'`,
|
||
validado em `termo`/`termo_especial`); o preço vem de
|
||
`ConsolidacaoEntrega.valor_para(tipo)`.
|
||
- `ConsolidacaoEntregasController#apontar_termo` lê `quantidade` (normal) +
|
||
`quantidade_especial` e cria **um lote por tipo** dentro de uma transação, auditando as
|
||
duas quantidades. `contar_manuais` soma a `quantidade` dos **dois** tipos de termo; os
|
||
agregados `group(:tipo).sum(:quantidade)` já funcionavam sem mudança.
|
||
- Stimulus: `apontamento_termo_controller.js` com targets `quantidade`/`quantidadeEspecial`
|
||
e ações `maisEspecial`/`menosEspecial`; `validacao_controller.js` com target opcional
|
||
`qtdTermoEspecial`.
|
||
|
||
### 📂 Arquivos
|
||
```
|
||
db/migrate/20260715000001_add_preco_termo_especial_configuracao.rb (nova)
|
||
app/models/configuracao.rb · consolidacao.rb · consolidacao_entrega.rb
|
||
app/controllers/consolidacao_entregas_controller.rb
|
||
app/javascript/controllers/apontamento_termo_controller.js · validacao_controller.js
|
||
app/views/consolidacao_entregas/validar.html.erb · revisar.html.erb
|
||
app/views/consolidacoes/show.html.erb
|
||
app/services/pdf/relatorio_motorista_pdf.rb
|
||
db/seeds.rb · spec/models/configuracao_spec.rb
|
||
```
|
||
|
||
```bash
|
||
docker-compose exec app bundle exec rails db:migrate
|
||
```
|
||
|
||
</details>
|
||
|
||
---
|
||
|
||
<details>
|
||
<summary><strong>🎨 Atualização 20/07/2026 — Ícones da marca (emoji → SVG laranja) + galeria de fotos no Editar Lançamento</strong></summary>
|
||
|
||
Três frentes num dia: (1) troca de **todos os ícones** do sistema, que eram emojis, por
|
||
ícones SVG chapados no laranja da marca; (2) **galeria com todas as fotos** do lançamento na
|
||
tela de Editar Lançamento; (3) investigação (com veredito) sobre **por que não dá para
|
||
editar as fotos** pela API do SimpliRoute. **Sem migration.**
|
||
|
||
### 🎨 1. Ícones — emoji → SVG da marca
|
||
Os ícones eram **emojis** (📊 📋 👥 ⚙️ …), que davam aparência "genérica de IA" e mudavam de
|
||
forma/cor conforme o sistema operacional. Agora são **SVG chapados**, na cor laranja da marca
|
||
(`#f97316`), consistentes em qualquer navegador.
|
||
|
||
- **Sprite próprio** em `public/icons.svg` — subconjunto do **Bootstrap Icons** (licença MIT),
|
||
arquivo único (~33 KB), servido localmente (sem CDN externo).
|
||
- **Helper `icone(nome, …)`** em `app/helpers/application_helper.rb`: mapa de nomes semânticos
|
||
em pt-BR (`:caminhao`, `:consolidacoes`, `:dinheiro`, …) → símbolo do sprite. Trocar um ícone
|
||
é mudar uma linha. Usa `fill="currentColor"` → a cor vem da classe CSS.
|
||
- **Helper `rotulo(nome, texto, …)`** para `link_to`/`button_to` (que recebem o rótulo como
|
||
argumento, onde não cabe ERB). `nav_link_to` ganhou a opção `icon:`.
|
||
- **~260 emojis substituídos em ~33 arquivos** (views, `_navbar`, layout, popups do mapa Leaflet
|
||
e HTML montado em JS na tela de Editar Lançamento).
|
||
- **Ícones de status mantêm cor semântica** (sucesso verde, alerta amarelo, erro vermelho) —
|
||
só os de navegação/ação viraram laranja.
|
||
- **E-mails** (`consolidacao_mailer`) ficaram sem os emojis decorativos: sprite via `<use href>`
|
||
não funciona em cliente de e-mail.
|
||
- **Ajuste fino (mesma data):** ícones **+20%** (padrão `1.05em`→`1.25em`), **espaçamento**
|
||
ícone↔texto (parâmetro `espaco:` com `mr-1`, desligado nos ícones sozinhos/centralizados) e
|
||
**correção de contraste** — ícone laranja sobre fundo laranja ficava invisível (botão
|
||
"Relatório" do dashboard e os ícones das telas de recuperar/redefinir senha → herdam preto).
|
||
|
||
### 🖼️ 2. Editar Lançamento — galeria com TODAS as fotos
|
||
Antes a tela mostrava só **uma** foto genérica. Descobrimos (testando a API real) que as fotos
|
||
reais do motorista **não** estão no array `pictures`, e sim em **`extra_field_values`**, em
|
||
campos nomeados: **`foto_nf`** (Nota Fiscal), **`foto_prova_visita`** (Prova da visita, presente
|
||
até em insucesso), **`foto_relatorio2`** (Relatório) e **`foto_termo`** (Termo, quase sempre vazio).
|
||
|
||
- Novo método **`SimpliRoute::Client#detalhe_visita`** → `GET /v1/plans/visits/{id}/detail/`
|
||
(fonte de fotos mais completa; best-effort, cai para a visita se falhar).
|
||
- **`fotos_do_lancamento`** no controller reúne **todas as fontes** — fachada (rastreio),
|
||
`extra_field_values`, `pictures[]` e `signature` — numa lista **etiquetada** (tipo + origem) e
|
||
**deduplicada por URL**.
|
||
- **Galeria** com a 1ª foto em destaque + grade, cada uma com etiqueta colorida por categoria,
|
||
e **visualizador (lightbox)** que amplia e navega (setas do teclado / Esc / link p/ tamanho real).
|
||
|
||
### 🚫 3. Por que NÃO dá para editar as fotos (limitação do SimpliRoute)
|
||
Investigamos todos os caminhos de escrita contra a API real. **A limitação é do SimpliRoute:**
|
||
|
||
| Caminho | Resultado |
|
||
|---|---|
|
||
| `PATCH`/`PUT` em `extra_field_values` (todos os formatos) | **HTTP 500** — o servidor deles quebra |
|
||
| Editar outros campos (`status`, `notes`) por `PATCH` | Funciona (200) — o bloqueio é **só** das fotos |
|
||
| Suspeita de "plano expirado" | Descartada — visita **ativa (de hoje)** dá o mesmo 500 |
|
||
| `GET /v1/plans/visits/{id}/detail/` | Somente leitura |
|
||
| Checkout mobile `POST /v1/mobile/visit/{id}/checkout/` | **403** — exige token de **motorista**; e refaz o checkout inteiro (hora/GPS/assinatura) |
|
||
| Webhook | Canal de **saída** apenas — não escreve de volta |
|
||
|
||
- **Webhook existente descoberto:** a conta (Gade Hospitalar) tem um webhook `visit_checkout_detailed`
|
||
apontando para um **Google Apps Script** (`script.google.com/…/exec`) — recebe os dados de cada
|
||
entrega finalizada (inclui as URLs das fotos). É **inbound**, não serve para editar.
|
||
- **Conclusão:** não há caminho administrativo (nem por API, nem pelo painel web do SimpliRoute)
|
||
para corrigir uma foto após o envio do motorista. Só liberando escrita em `extra_field_values`
|
||
(pela API) ou uma opção de substituir imagem no painel deles.
|
||
- **Ferramenta de diagnóstico:** `bin/sondar_fotos_simpliroute --visita <id> [--testar-escrita]` —
|
||
mostra as fotos de todas as fontes e testa (com segurança, mirando campo vazio e restaurando) se
|
||
a escrita é aceita. Roda no servidor: `docker compose exec app bin/sondar_fotos_simpliroute …`.
|
||
|
||
> ⚠️ **Token do SimpliRoute:** é de **produção** e tem escrita. Se for compartilhado (chat/e-mail),
|
||
> **rotacione** depois em `app2.simpliroute.com`.
|
||
|
||
### 📂 Arquivos
|
||
```
|
||
public/icons.svg (novo — sprite Bootstrap Icons, MIT)
|
||
bin/sondar_fotos_simpliroute (novo — sondagem da API de fotos)
|
||
app/helpers/application_helper.rb (icone / rotulo / nav_link_to)
|
||
app/services/simpli_route/client.rb (detalhe_visita)
|
||
app/controllers/admin/edicao_lancamentos_controller.rb (fotos_do_lancamento)
|
||
app/views/admin/edicao_lancamentos/show.html.erb (galeria + lightbox)
|
||
app/views/layouts/_navbar.html.erb · _sidebar.html.erb · application.html.erb
|
||
app/javascript/controllers/validacao_controller.js (chip usa SVG, não emoji)
|
||
+ ~30 views com emojis substituídos (consolidacoes, dashboard, operacoes_dashboard, devise, …)
|
||
```
|
||
|
||
> **Sem migration e sem gem nova** — só views, helpers, JS e um asset estático.
|
||
> Em produção, o `public/icons.svg` precisa ir junto no deploy.
|
||
|
||
</details>
|
||
|
||
---
|
||
|
||
<details>
|
||
<summary><strong>🔁 NF com mais de um lançamento + varredura de responsividade (21–22/07/2026)</strong></summary>
|
||
|
||
## Parte 1 — Editar Lançamento: quando a mesma NF tem 2 visitas
|
||
|
||
### 🎯 O problema real
|
||
Quando um plano é **duplicado** no SimpliRoute, nasce uma **visita nova** (outro `tracking_id`) com a
|
||
**mesma NF**, e a antiga continua existindo. Caso que motivou tudo: **NF 82891** com visita em
|
||
**17/07** (pendente, motorista Thiago Rabello Bittencourt) e outra em **21/07** (sucesso, sem
|
||
motorista). A tela mostrava **uma só** — e depois, por regressão, **nenhuma**.
|
||
|
||
### 🔴 Causas corrigidas (foram 7, em camadas)
|
||
1. **`Entrega.por_nf(nf).first`** — pegava uma linha só, sem `ORDER BY`. Agora carrega todas e a
|
||
tela lista as ocorrências para o ADM escolher qual editar.
|
||
2. **`SimpliRoute::Client#resolver_id` abortava na ambiguidade** (`"Mais de uma visita para a NF…"`)
|
||
em vez de deixar escolher. A tela de edição não passa mais por ele.
|
||
3. **As datas vinham só do espelho local.** Como a API **não busca NF por intervalo**, a visita de
|
||
17/07 era inalcançável se o painel só conhecia a de 21/07. Entraram campos **De/Até** + botão
|
||
**"Últimos 30 dias"** (varredura dia a dia, teto de 62 dias).
|
||
4. **Um dia com falha derrubava a busca inteira** → agora é *fail-soft* por dia: registra em
|
||
`falhas` e segue. A tela informa o período consultado e quais dias falharam.
|
||
5. **`carregar?` faltando em `EdicaoLancamentoPolicy`** → o Pundit levantava `NoMethodError` (500),
|
||
o `fetch` recebia HTML e o JS reportava "erro de conexão". Falhou **fechado** (negou acesso),
|
||
sem brecha de segurança.
|
||
6. **Tela em branco:** `renderOcorrencias()` montava a lista inteira mas **nunca removia a classe
|
||
`hidden`** do container. O conteúdo estava no DOM (contador já dizia "2 lançamentos"), invisível.
|
||
7. **Fuso horário:** `new Date('2026-07-15')` é meia-noite **UTC** e, em UTC−3, o `toLocale`
|
||
devolvia **14/07**. Datas puras agora são formatadas direto do texto ISO; horários de checkout
|
||
continuam convertendo (correto para instante).
|
||
|
||
### 🔑 Descobertas sobre a API do SimpliRoute (medidas contra a API real, 21/07/2026)
|
||
| Parâmetro | Resultado |
|
||
|---|---|
|
||
| **`&search=<NF>`** | ✅ **Funciona e não é documentado.** Um dia cai de **3,8 MB / ~9 s** (1879 visitas) para **~1 KB / ~0,6 s**. Varredura de 31 dias em lotes paralelos: **3,4 s / 3,2 KB** |
|
||
| `reference`, `reference_id`, `q`, `title`, `reference__*` | ❌ **Ignorados em silêncio** — devolvem 200 com o dia inteiro |
|
||
| `planned_date_from/to`, `since/until`, `__gte/__lte`, `date_from/to` | ❌ **Não existe filtro de intervalo** — todos devolveram 2469, idêntico ao controle **sem parâmetro nenhum** |
|
||
| `GET /v1/routes/visits/` sem `planned_date` | ⚠️ Devolve um **conjunto padrão (~2469)** que **não cobre o histórico** — para a NF 82891 voltava só a visita de 21/07 e escondia a de 17/07 |
|
||
|
||
> ⚠️ **Lição:** parâmetro não registrado no backend Django é **ignorado sem erro**. "Voltou 200 com
|
||
> resultados" **não** prova que filtrou — a prova é a **contagem diminuir** em relação ao dia sem o
|
||
> filtro. Ferramenta: **`bin/sondar_busca_nf --nf <n> --data <YYYY-MM-DD> [--sem-data]`** (só GET).
|
||
|
||
---
|
||
|
||
## Parte 2 — Responsividade
|
||
|
||
### 🎯 A raiz comum
|
||
Os breakpoints do Tailwind (`md:`, `lg:`, `xl:`) enxergam a **janela**, mas o conteúdo perde
|
||
**256px** para a sidebar (`main.md:ml-64`). **A mesma janela de 1280px tem duas larguras** conforme
|
||
o menu esteja aberto ou recolhido — e grades de colunas fixas não ficam sabendo. Daí "quando a barra
|
||
de menu é acionada, quebra o layout". Solução: **`flex-wrap` / `auto-fit + minmax`**, que reagem ao
|
||
espaço **real**.
|
||
|
||
### 🔧 Corrigido
|
||
| Tela | Antes | Sintoma | Depois |
|
||
|---|---|---|---|
|
||
| `consolidacoes/index` | `md:grid-cols-6` | Campos a ~150px; texto do botão **"Filtrar" vazava** para fora do fundo | `flex-wrap` + `basis-*` |
|
||
| `dashboard/index` | `xl:grid-cols-5` | **"R$ 82.692,"** — valor cortado pelo `overflow-hidden` do card | `auto-fit,minmax(13rem,1fr)` |
|
||
| `consolidacao_entregas/revisar` | `md:grid-cols-7` | 7 cards de **~55px**, destruindo "Extraordinária"/"Termo Especial" | `auto-fit,minmax(9rem,1fr)` |
|
||
| `configuracoes` + `admin/configuracoes` | `lg:grid-cols-4` | ~128px úteis para valores em moeda | `auto-fit,minmax(14rem,1fr)` |
|
||
|
||
**Mantidos de propósito:** `validar.html.erb` (`lg:grid-cols-4` é a divisão 3:1 lista/Resumo, não
|
||
grade de cards) e os `xl:grid-cols-3` de gráficos — nesses o conteúdo encolhe sem cortar.
|
||
|
||
### 📐 Passo 2 (validar) — cabeçalho fixo
|
||
Medido a 1280×577: **442px de cabeçalho contra 67px de lista** (menos que um card).
|
||
|
||
- Voltar + título + progresso passaram a **uma faixa só**; o card de progresso (70px de moldura para
|
||
uma barra de 12px) virou linha de 8px.
|
||
- **"Adicionar lançamento"** entrou na barra de ações via `order` do flex — sem mover os ~170 linhas
|
||
de modais que vivem dentro do `data-controller`.
|
||
- **Pilares (Normal/Retirada/Bônus/Desconto/Extra) continuam SEMPRE visíveis**, apenas **esmaecidos**
|
||
(`opacity-40`) enquanto não há seleção, com contador e "limpar" aparecendo ao selecionar.
|
||
⚠️ Uma versão intermediária os **escondia** até haver seleção — revertido: são a ação principal da
|
||
tela e sumir com eles esconde o que dá para fazer de quem ainda não sabe que precisa selecionar.
|
||
- Legibilidade preservada: título `text-2xl`, subtítulo e progresso `text-sm`, números em branco/negrito.
|
||
|
||
**Resultado:** cabeçalho **442px → 210px**, lista **67px → 299px**.
|
||
|
||
### 📂 Arquivos
|
||
```
|
||
app/controllers/admin/edicao_lancamentos_controller.rb # lista ocorrências, período, fail-soft, carregar
|
||
app/policies/edicao_lancamento_policy.rb # + carregar? (era o 500)
|
||
app/services/simpli_route/client.rb # visitas_da_data(data, busca:) → &search=
|
||
app/views/admin/edicao_lancamentos/show.html.erb # lista de ocorrências, De/Até, avisos, fuso
|
||
config/routes.rb # + get :carregar
|
||
bin/sondar_busca_nf # NOVO — sondagem de filtros da API (só GET)
|
||
app/views/consolidacao_entregas/validar.html.erb # cabeçalho compacto + barra de ações
|
||
app/javascript/controllers/validacao_controller.js # atualizarSelecao / limparSelecao
|
||
app/views/consolidacao_entregas/revisar.html.erb # grid-cols-7 → auto-fit
|
||
app/views/consolidacoes/index.html.erb # filtros → flex-wrap
|
||
app/views/dashboard/index.html.erb # KPIs → auto-fit
|
||
app/views/configuracoes/index.html.erb # preços → auto-fit
|
||
app/views/admin/configuracoes/index.html.erb # idem (arquivo distinto, também vivo)
|
||
```
|
||
|
||
### ⚠️ Pendências e alertas
|
||
- **Tailwind vem do Play CDN** (`cdn.tailwindcss.com`, em `application.html.erb:15`), que gera CSS no
|
||
navegador em tempo real. A documentação oficial **desaconselha em produção** (~380KB bloqueando a
|
||
renderização, recompilação a cada carregamento). O **`tailwind.config.js` do repositório não está
|
||
sendo usado** — a config real está embutida no layout (linha 17), e a gem `tailwindcss-rails` está
|
||
no Gemfile sem servir CSS.
|
||
- **Não existe teste para `Admin::EdicaoLancamentosController`** (`spec/` não tem nada de
|
||
`edicao_lancamento`). Duas das quebras acima — policy faltando e `hidden` não removido — seriam
|
||
pegas por um teste de request/sistema em segundos, sem custar deploy.
|
||
- **Token do SimpliRoute:** se passou por chat/e-mail, **rotacione** em `app2.simpliroute.com`.
|
||
|
||
> **Sem migration e sem gem nova** — controller, policy, service, views e JS.
|
||
> ⚠️ Reiniciar o Puma após o deploy (cache de classes/views).
|
||
|
||
</details>
|
||
|
||
---
|
||
|
||
<details>
|
||
<summary><strong>✉️ Notificações e E-mail configuráveis pela tela — SMTP + WhatsApp (11/08/2026)</strong></summary>
|
||
|
||
> ⚠️ **STATUS: implementado, ainda NÃO executado.** Nada aqui foi rodado contra um banco nem
|
||
> contra os servidores reais (Gmail/Twilio) — não há Ruby nem Postgres na máquina de
|
||
> desenvolvimento. Foi conferida a sintaxe de todos os arquivos `.rb` e `.erb` e validado o
|
||
> algoritmo de normalização de telefone em Ruby puro. **A migration, a suíte e o envio real
|
||
> continuam pendentes** — roteiro no fim desta seção.
|
||
|
||
### 🎯 O problema
|
||
Servidor de e-mail e credenciais do Twilio viviam **só no `.env`**: trocar a senha de app do Gmail
|
||
ou o número remetente exigia editar o arquivo no servidor e **reiniciar o container**. O ADM não
|
||
tinha como fazer nada disso pela interface.
|
||
|
||
Pior: as chaves `notificacao_whatsapp` e `notificacao_email` existiam em `configuracoes` mas
|
||
**nunca tiveram UI** — `Admin::ConfiguracoesController#index` filtra por `CHAVES_MOEDA`. Ligar
|
||
notificação só era possível pelo `rails console`.
|
||
|
||
### 🆕 A tela
|
||
**Configurações → card "Notificações e E-mail"** (`/admin/configuracao_notificacao`).
|
||
**Só `admin`** — `ConfiguracaoNotificacaoPolicy` é mais restrita que `ConfiguracaoPolicy`, que
|
||
libera `index?` para gerente: aqui ficam senha de e-mail e token do Twilio.
|
||
|
||
O acesso é **exclusivamente pelo card dentro de Configurações** — de propósito não há item no menu
|
||
lateral, para não expor um atalho de credenciais na navegação de todo dia.
|
||
|
||
| Bloco | Campos |
|
||
|---|---|
|
||
| **Servidor de e-mail (SMTP)** | ativo, servidor, porta, usuário, senha, autenticação, domínio, e-mail e nome do remetente + toggle "avisar motoristas por e-mail" |
|
||
| **WhatsApp (Twilio)** | ativo, Account SID, Auth Token, número remetente |
|
||
| **Destinatários administrativos** | e-mail do admin, WhatsApp do admin |
|
||
|
||
Três botões: **Salvar**, **Salvar e enviar e-mail de teste**, **Salvar e enviar WhatsApp de teste**.
|
||
|
||
### ⚙️ Como funciona (técnico) — os 6 pontos não-óbvios
|
||
|
||
**1. Hierarquia banco > `.env`, sem quebrar nada.** `ConfiguracaoNotificacao#smtp_settings` devolve
|
||
**`nil`** quando não está pronto. O ActionMailer faz `.merge(options || {})` por cima do que o
|
||
`config/initializers/smtp.rb` montou no boot — então o fallback para o `.env` é **automático**.
|
||
Enquanto os toggles estiverem desligados, o comportamento é **idêntico ao de antes desta tela**.
|
||
|
||
**2. `default delivery_method_options:`, NÃO um `before_action`.** Um callback que mexesse em
|
||
`message.delivery_method` seria **descartado**: `ActionMailer::Base#mail` roda *depois* dos
|
||
callbacks e chama `wrap_delivery_behavior!`, que reconfigura a mensagem. O
|
||
`delivery_method_options` é lido *dentro* do próprio `mail()`.
|
||
|
||
**3. `proc`, NÃO lambda.** O Devise avalia o `default from:` com `instance_eval(&proc)`, que passa
|
||
1 argumento. Um `-> { }` de aridade 0 estouraria **`ArgumentError` em todo "esqueci minha senha"**.
|
||
|
||
**4. `config.parent_mailer = 'ApplicationMailer'`** no `devise.rb` — sem isso o reset de senha
|
||
continuaria preso ao `.env`. Efeito colateral aceito: os e-mails do Devise passam a usar
|
||
`app/views/layouts/mailer.html.erb`.
|
||
|
||
**5. Segredos cifrados sem `master.key`.** Senha SMTP e Auth Token vão para colunas
|
||
`*_cifrado` (AES-256-GCM) via `AtributoCifrado`, com chave derivada do `secret_key_base`. **Não** se
|
||
usou ActiveRecord Encryption: exigiria 3 chaves novas, dependeria da ordem dos initializers e
|
||
estouraria `Errors::Decryption` na leitura. Aqui o reader faz `rescue → nil`, o app **degrada para
|
||
o `.env`** e a tela mostra um banner amarelo pedindo para redigitar.
|
||
|
||
**6. Testes com `deliver_now` e `raise_delivery_errors = true` forçado.** O `development.rb` define
|
||
`raise_delivery_errors = false` e o adapter do ActiveJob é `:async` (thread in-process) — com
|
||
`deliver_later` **o teste "passaria" em silêncio mesmo com a senha errada**.
|
||
|
||
### 🩹 Bug pré-existente corrigido de passagem
|
||
`NotificacaoService` mandava `to: "whatsapp:#{user.telefone}"` com o telefone **cru do cadastro**.
|
||
Um telefone gravado como `(11) 92005-1157` vira `whatsapp:(11) 92005-1157`, o Twilio devolve
|
||
**21211** — e o `rescue` engolia. **Provavelmente nenhum WhatsApp a motorista jamais chegou.**
|
||
Agora passa por `ConfiguracaoNotificacao.canal`, que normaliza para E.164.
|
||
|
||
> Casos cobertos: `11 920051157`, `(11) 92005-1157`, `011 …`, `+55 11 …`, `whatsapp:+55…` →
|
||
> `+5511920051157`. O prefixo `55` só é removido quando sobra número demais — senão quebraria o
|
||
> **DDD 55** (Santa Maria/RS), onde `55991234567` já é o número completo.
|
||
|
||
### 🔐 Segurança
|
||
- A senha gravada **nunca volta para o HTML** (`password_field value: nil`). Campo em branco
|
||
significa "mantenha a atual" — salvar sem redigitar não apaga o que está lá.
|
||
- `AuditoriaLog` registra a mudança com `acao: 'editar_notificacoes'`, mas grava apenas
|
||
`smtp_password_definida: true/false`. **Nunca a senha nem o token** — `dados_novos` é exibido em
|
||
`/admin/auditoria_logs`, que **gerente também acessa**.
|
||
- Os erros do Twilio vêm traduzidos (63003/63015 = falta o `join <palavra>` do sandbox, 21608 =
|
||
conta trial só envia a número verificado, 20003 = SID/token inválidos…), para o ADM resolver
|
||
sozinho sem abrir o log do container.
|
||
|
||
### 🆕 Migration adicionada (rodar `db:migrate`)
|
||
```
|
||
20260811000001_create_configuracao_notificacoes.rb
|
||
```
|
||
Tabela **singleton** (índice único em `singleton_guard` — impede dois workers Puma criarem linhas
|
||
concorrentes). Copia as flags antigas de `configuracoes` e **não** importa credenciais do `.env`:
|
||
os campos nascem vazios e o fallback segue mandando até alguém preencher a tela.
|
||
|
||
### 📂 Arquivos
|
||
```
|
||
db/migrate/20260811000001_create_configuracao_notificacoes.rb # NOVO — tabela singleton
|
||
app/models/configuracao_notificacao.rb # NOVO — smtp_settings, credenciais, E.164
|
||
app/models/concerns/atributo_cifrado.rb # NOVO — AES-256-GCM sem master.key
|
||
app/policies/configuracao_notificacao_policy.rb # NOVO — show?/update? = admin
|
||
app/controllers/admin/configuracao_notificacoes_controller.rb # NOVO — show/update + params[:acao]
|
||
app/views/admin/configuracao_notificacoes/show.html.erb # NOVO — a tela
|
||
app/services/notificacao/resultado.rb # NOVO — ok?/mensagem/detalhe → flash
|
||
app/services/notificacao/teste_email.rb # NOVO — erros SMTP em português
|
||
app/services/notificacao/teste_whatsapp.rb # NOVO — códigos Twilio em português
|
||
app/services/notificacao/cliente_twilio.rb # NOVO — client com timeout de 15s
|
||
app/mailers/teste_mailer.rb · app/views/teste_mailer/teste.html.erb # NOVO
|
||
app/services/notificacao_service.rb # lê do banco; unifica os 2 pares duplicados
|
||
app/mailers/application_mailer.rb # default from: / delivery_method_options: proc
|
||
app/mailers/consolidacao_mailer.rb # removido o `default from:` que anulava o proc
|
||
config/initializers/devise.rb # + parent_mailer
|
||
config/initializers/smtp.rb # vira fallback (só comentário)
|
||
config/initializers/inflections.rb # + irregular 'notificacao'
|
||
config/routes.rb # + resource :configuracao_notificacao
|
||
app/views/admin/configuracoes/index.html.erb # + card "Notificações e E-mail" (só admin)
|
||
app/models/configuracao.rb # marca notificacao_* como obsoletas
|
||
.env.example # hierarquia banco > .env + NOTIFICACAO_SECRET
|
||
spec/models/configuracao_notificacao_spec.rb # NOVO
|
||
spec/policies/configuracao_notificacao_policy_spec.rb # NOVO
|
||
spec/requests/admin/configuracao_notificacoes_spec.rb # NOVO
|
||
spec/models/table_names_spec.rb # + ConfiguracaoNotificacao
|
||
```
|
||
|
||
### ⏳ Pendente de execução — roteiro
|
||
```bash
|
||
# 1. Migrar
|
||
docker-compose exec app bundle exec rails db:migrate
|
||
|
||
# 2. Suíte
|
||
docker-compose exec app bundle exec rspec spec/models spec/policies spec/requests
|
||
|
||
# 3. Confirmar a assinatura da gem (não pôde ser verificada fora do container)
|
||
docker-compose exec app bundle exec rails runner \
|
||
'p Twilio::HTTP::Client.instance_method(:initialize).parameters'
|
||
```
|
||
4. **Permissão:** logar como **gerente** → o card não aparece e `/admin/configuracao_notificacao`
|
||
redireciona com "Você não tem permissão". Como **admin** → a tela abre.
|
||
5. **E-mail:** `smtp.gmail.com`, porta 587, **Senha de app de 16 caracteres** (não a senha da
|
||
conta), marcar "Ativar envio de e-mail" → **Salvar e enviar e-mail de teste**. Conferir o spam.
|
||
6. **WhatsApp:** SID / Auth Token / número remetente do Twilio, marcar "Ativar WhatsApp" →
|
||
**Salvar e enviar WhatsApp de teste**. ⚠️ **No sandbox, o número que vai RECEBER precisa antes
|
||
mandar `join <sua-palavra>`** para o número do sandbox — sem esse opt-in volta 63003/63015.
|
||
7. **Não regressão:** com os toggles desligados, finalizar uma consolidação e conferir que nada
|
||
mudou; e testar o **"Esqueci minha senha"**, que trocou de mailer pai.
|
||
|
||
### ⚠️ Alertas
|
||
- **Rotação do `SECRET_KEY_BASE` torna senha e token ilegíveis.** O sistema não quebra (volta ao
|
||
`.env` e avisa na tela), mas os dois campos precisam ser redigitados. Para desacoplar, defina
|
||
**`NOTIFICACAO_SECRET`** no `.env` com uma string longa e **fixa**.
|
||
- **Não cachear a config em `Rails.cache`:** o `production.rb` usa `:memory_store`, que é por
|
||
processo — a tela pareceria "não salvar" para os outros workers. É 1 `SELECT` por e-mail.
|
||
- `app/views/configuracoes/index.html.erb` (fora do `admin/`) **não recebeu o card**: não tem rota
|
||
e é código morto — o vivo é `app/views/admin/configuracoes/index.html.erb`.
|
||
|
||
> **Migration nova** (`db:migrate` obrigatório) e **sem gem nova** — `twilio-ruby` já estava no
|
||
> Gemfile. ⚠️ Reiniciar o Puma após o deploy.
|
||
|
||
</details>
|
||
|
||
|
||
---
|
||
|
||
<details>
|
||
<summary><strong>💰 Entrega sem sucesso entra no pagamento + aba Consolidado no ranking (20–21/08/2026)</strong></summary>
|
||
|
||
> ⚠️ **STATUS: no ar no ambiente de teste e conferido com dados reais** (17 motoristas,
|
||
> 4.976 entregas de agosto). O que **continua pendente é a suíte** — não há Ruby nem Postgres na
|
||
> máquina de desenvolvimento, então os specs novos foram validados só na sintaxe. Roteiro no fim
|
||
> desta seção.
|
||
|
||
### 🎯 O problema
|
||
A Reem **paga a entrega sem sucesso**: o motorista foi até o local, teve o deslocamento e o custo,
|
||
e o insucesso é só o desfecho da visita. A **consolidação** já tratava assim desde sempre
|
||
(`Entrega::STATUS_ATENDIDO` = `completed` + `failed`), mas o **dashboard não** — mostrava um valor
|
||
**menor que o do fechamento**, e ninguém sabia explicar a diferença.
|
||
|
||
### 🔴 Correções
|
||
|
||
**1. Dashboard principal contava só as concluídas** — `dashboard_controller.rb` (commit `cebd6e2`)
|
||
|
||
A base financeira era `Entrega.pagas` (só `completed`). Passou a ser `.atendidas`
|
||
(`completed` + `failed`, com checkout) — **o mesmo recorte que a consolidação considera elegível**,
|
||
que é justamente o ponto: tela e fechamento agora partem do mesmo conjunto.
|
||
|
||
**2. A falhada caía no período errado** — mesmo commit
|
||
|
||
As falhas eram filtradas por `planned_date`, e as concluídas por `checkout`. **Falhada TEM
|
||
checkout** (o motorista fechou a visita com motivo de insucesso), então o eixo correto é o mesmo
|
||
das concluídas. Uma entrega planejada em 31/07 e fechada em 01/08 pertence a agosto — como a
|
||
consolidação sempre entendeu.
|
||
|
||
**3. O painel do motorista ficou para trás** — `motorista/dashboard_controller.rb` (commit `4965931`)
|
||
|
||
Continuava em `Entrega.pagas` + `no_periodo` (planned_date), ou seja, a lógica antiga inteira.
|
||
O motorista via **menos do que ia receber** — no mês corrente, 124 entregas (~R$ 2.232,00)
|
||
invisíveis — e a diferença só aparecia no fechamento. Passou para `.atendidas` +
|
||
`no_periodo_checkout`, e o rótulo *"N entregas feitas e confirmadas"*, que mentia sobre o número
|
||
novo, virou:
|
||
|
||
```
|
||
R$ 20,00
|
||
2 entregas atendidas
|
||
1 entregues · 1 sem sucesso (pagas também)
|
||
```
|
||
|
||
A segunda linha não é enfeite: sem ela o motorista vê um total maior e não tem como conferir de
|
||
onde veio.
|
||
|
||
**4. A barra do ranking contradizia a ordem do ranking** — `_ranking_motoristas.html.erb` (commit `e9db6b7`)
|
||
|
||
A barra era proporcional à **quantidade**, mas o card é ordenado por **valor**. Na aba Estimado dá
|
||
no mesmo (valor = qtd × preço); na Consolidada, bônus/retirada/termo mudam o preço unitário e a
|
||
barra do 3º (290 entregas, R$ 5.250) saía **maior** que a do 2º (261 entregas, R$ 5.260).
|
||
Invertia em três pontos da lista. Agora escala pelo valor, que é o número que ordena.
|
||
|
||
### 🆕 Aba "Consolidado" no ranking de motoristas
|
||
|
||
O card **Motoristas** ganhou duas abas — a dúvida recorrente era justamente *"esse ranking mostra o
|
||
estimado ou o consolidado?"*:
|
||
|
||
| Aba | O que mostra | De onde vem |
|
||
|-----|--------------|-------------|
|
||
| **Estimado** | entregas atendidas × preço da entrega | espelho de rastreio (`Entrega.atendidas`) |
|
||
| **Consolidado** | valor **realmente fechado** + quantidade exata de entregas | `consolidacao_motoristas` / `consolidacao_entregas` |
|
||
|
||
Os números divergem **de propósito**: o estimado cobre tudo que foi atendido no período; o
|
||
consolidado, só o que já entrou em consolidação **finalizada**, com bônus/desconto/retirada
|
||
aplicados. Cada aba diz na tela de onde vem o seu número.
|
||
|
||
Dois detalhes decidem se a quantidade sai certa:
|
||
|
||
- **`DISTINCT tracking_id`, não contagem de linhas.** Uma entrega pode ter vários pilares —
|
||
Normal + Bônus + Retirada são **3 linhas** em `consolidacao_entregas` para **1 entrega**.
|
||
Contar linhas inflaria o número.
|
||
- **Só motoristas ativos.** O ranking parte de `@fin_por_motorista` (que vem de
|
||
`ConsolidacaoMotorista.ativos`), então **arquivado não aparece** — ele saiu do fechamento e não
|
||
tem valor a exibir. Isso também garante que a aba soma exatamente o KPI "Custo total" do topo.
|
||
|
||
O markup da lista virou a partial `_ranking_motoristas.html.erb`, usada pelas duas abas: os dois
|
||
conjuntos têm a mesma forma (`:nome`, `:valor`, `:entregas`) e duplicar o HTML faria as abas
|
||
divergirem visualmente na primeira alteração.
|
||
|
||
### ✅ Conferência com dados reais (teste.reemtransportes.com.br, 21/08/2026)
|
||
|
||
**Dashboard principal — período 01/08 a 21/08:**
|
||
|
||
| Card | Na tela | Confere |
|
||
|------|---------|---------|
|
||
| Valor Estimado | R$ 89.568,00 | 4.976 × R$ 18,00 exato |
|
||
| — subtítulo | 4.976 entregas atendidas | 4.852 + 124 |
|
||
| Total Entregas | 4.977 | 4.976 atendidas + 1 pendente |
|
||
|
||
O teste decisivo: 4.852 concluídas × R$ 18 dariam **R$ 87.336,00**. A tela mostra **R$ 89.568,00** —
|
||
exatamente **R$ 2.232,00 a mais, que são as 124 sem sucesso**. O insucesso entra no dinheiro, não
|
||
só na contagem.
|
||
|
||
**Consistência interna** (as falhas entram em todo lugar, não só no card):
|
||
- Ranking por motorista: os 17 motoristas somam **exatamente 4.976**; se contasse só sucesso daria
|
||
4.852.
|
||
- Gráfico "Evolução do custo": a série diária soma **R$ 69.408,00**, idêntico ao valor estimado do
|
||
período 01–14/08 — as falhas caem nos dias certos (eixo checkout).
|
||
- Sem dupla contagem: o scope `pendentes` exclui `failed`, então entregue / pendente / sem sucesso
|
||
não se sobrepõem.
|
||
|
||
**Aba Consolidado — reconciliação com os KPIs:**
|
||
|
||
| | Soma da aba | KPI do topo |
|
||
|---|---|---|
|
||
| Valores | **R$ 72.508,00** | R$ 72.508,00 ("Custo total") ✅ |
|
||
| Entregas | **3.858** | 3.858 ("entregas classificadas") ✅ |
|
||
|
||
Bate à vírgula e à unidade. Isso explica também a diferença **3.858 consolidadas × 3.856 atendidas**:
|
||
são 2 entregas que entraram no fechamento sem estar na janela de checkout do período (apontamento
|
||
manual de NF fora do período, ou consolidação que extrapola as datas). **Não é erro de contagem —
|
||
são bases diferentes**, e agora dá para ver as duas lado a lado.
|
||
|
||
### 🧪 Specs — ⏳ pendentes de execução
|
||
|
||
`spec/requests/dashboard_spec.rb` (+4 casos) e `spec/requests/motorista_dashboard_spec.rb` (novo,
|
||
5 casos). Usam o harness `spec/support/espelho_rastreio.rb`, que monta uma cópia descartável de
|
||
`db_reem_simplerout_2026` no banco de teste — sem ele só daria para mockar o método, o que não pega
|
||
regressão de **SQL**, que é onde moram os bugs de eixo de data.
|
||
|
||
O que fica travado:
|
||
- a sem sucesso soma no valor e na quantidade;
|
||
- entra pelo **checkout** (planejada 31/07 + checkout 01/08 → conta em agosto) e sai quando o
|
||
checkout cai fora;
|
||
- pendente sem checkout não vira dinheiro;
|
||
- entrega de outro motorista não vaza para o painel;
|
||
- na aba Consolidado: 3 linhas de 2 `tracking_id` = **"2 entregas"**, desconto subtraindo,
|
||
arquivado fora da lista, rascunho não entrando.
|
||
|
||
> As asserções da aba Consolidado são escopadas ao `#ranking-painel-consolidado` via Nokogiri — a
|
||
> aba Estimado renderiza o **mesmo markup** (moeda + "N entregas"), então asserção no `body` inteiro
|
||
> passaria por acidente.
|
||
|
||
O painel do motorista não tem filtro de período (é sempre "do dia 1º até hoje"), então o spec
|
||
congela a data com `travel_to`; sem isso ele quebraria sozinho ao rodar no dia 1º.
|
||
|
||
### 📂 Arquivos
|
||
```
|
||
app/controllers/dashboard_controller.rb (atendidas + eixo checkout; @ranking_consolidado)
|
||
app/controllers/motorista/dashboard_controller.rb (atendidas + no_periodo_checkout; quebra do card)
|
||
app/models/entrega.rb (scopes atendidas / falhadas / no_periodo_checkout)
|
||
app/views/dashboard/index.html.erb (abas Estimado/Consolidado + JS da troca)
|
||
app/views/dashboard/_ranking_motoristas.html.erb (NOVO — lista compartilhada pelas duas abas)
|
||
app/views/motorista/dashboard/index.html.erb (rótulo "atendidas" + linha da quebra)
|
||
spec/requests/dashboard_spec.rb (+ aba Consolidado)
|
||
spec/requests/motorista_dashboard_spec.rb (NOVO)
|
||
spec/support/espelho_rastreio.rb (harness da tabela externa)
|
||
```
|
||
|
||
> **Sem migration e sem gem nova** — só controllers, views e specs.
|
||
|
||
### ⏳ Pendente — roteiro
|
||
```bash
|
||
# 1. Suíte (única coisa que não pôde ser executada)
|
||
docker compose exec app bundle exec rspec \
|
||
spec/requests/dashboard_spec.rb spec/requests/motorista_dashboard_spec.rb
|
||
|
||
# 2. Depois do deploy: conferir a barra do ranking na aba Consolidado
|
||
# (deve encurtar sempre de cima para baixo)
|
||
|
||
# 3. Painel do motorista com dado real — logar como motorista no teste e
|
||
# conferir a linha "N entregues · N sem sucesso (pagas também)"
|
||
```
|
||
|
||
</details>
|
||
|
||
---
|
||
|
||
<details>
|
||
<summary><strong>🔢 Dashboard × Operações: por que os números não batiam — notas x visitas + painel de avulsas (24/08/2026)</strong></summary>
|
||
|
||
> ⚠️ **STATUS: implementado, ainda NÃO executado.** Não há Ruby/Bundler nem Postgres na máquina de
|
||
> desenvolvimento — foi conferida a sintaxe de todos os `.rb` e `.erb` alterados. **A suíte e a
|
||
> validação com dado real continuam pendentes** — roteiro no fim desta seção.
|
||
|
||
### 🎯 O problema
|
||
Mesmo período filtrado, dois números diferentes:
|
||
|
||
| | Dashboard financeiro | Dashboard de Operações |
|
||
|---|---|---|
|
||
| Total | 4977 | 4973 |
|
||
| Entregues / Sucesso | 4852 | 4851 |
|
||
| Falhadas / Recusas | 124 | 122 |
|
||
| Pendentes | 1 | 0 |
|
||
|
||
Não era arredondamento: **as duas telas contam coisas diferentes**, e nada na interface dizia isso.
|
||
|
||
- O **financeiro** conta **visitas** (idas ao local). É o recorte certo lá, porque é por ida que o
|
||
motorista recebe — `Entrega.contar_atendidas` conta linhas, e a consolidação paga em cima disso.
|
||
- **Operações** conta **notas fiscais** (último status de cada NF). É o recorte certo aqui, porque
|
||
é o que o cliente paga e o que confere nos documentos físicos — o mesmo critério da aba ENTREGAS
|
||
da planilha entregue (`Analytics::PlanilhaEntregas`).
|
||
|
||
Uma NF que falhou dia 10 e foi entregue dia 12 vale **2 no financeiro e 1 em Operações**. Correto
|
||
nos dois — mas invisível.
|
||
|
||
### 🐛 Três defeitos reais por trás disso
|
||
|
||
**1. O dedup rodava sobre a tabela inteira, não sobre o período.** O `ROW_NUMBER() ... rn = 1`
|
||
ficava numa CTE **antes** do filtro de data. Se o último checkout de uma NF era **posterior** ao fim
|
||
do período, a visita que aconteceu **dentro** do período sumia da contagem do mês. Subcontagem
|
||
silenciosa em todo fechamento. Agora o dedup acontece em `#linhas`, **depois** do período e do
|
||
cross-filter.
|
||
|
||
**2. O dedup escondia todo o insucesso reentregue.** NF que falhou duas vezes antes de entregar
|
||
aparecia como 100% de sucesso, e o motivo sumia do "Índices de falha".
|
||
|
||
**3. O período nem era aplicado no modo Operação.** `montar([@operacao], ...)` era chamado **sem
|
||
`inicio:`/`fim:`** e o seletor de data só aparecia no modo Global — comparar as duas telas "no mesmo
|
||
período" era literalmente impossível.
|
||
|
||
### 🆕 O que mudou na tela
|
||
|
||
**Operações — camada "Visitas ao local"** (abaixo dos 4 cards): visitas realizadas, retentativas,
|
||
insucessos por visita e "entregues na 2ª ida ou mais". Os 4 cards de cima seguem contando **notas**.
|
||
|
||
**Operações — painel "Notas fora da operação"**: NFs entregues no período que **não estão em nenhuma
|
||
planilha `gade_entregas_*`** — os planos avulsos e de inclusão. Contavam no financeiro e o
|
||
`INNER JOIN` com a tabela da operação as descartava aqui. Agora aparecem com NF, plano/título,
|
||
motorista, unidade, data, resultado e motivo, com quebra por plano de origem.
|
||
|
||
**Operações — período no modo Operação**: o seletor passa a valer também aqui, mas **só quando o
|
||
operador escolhe uma faixa** (`inicio`/`fim` na URL). Sem escolha, o recorte segue sendo a operação
|
||
inteira — senão abrir uma operação de meses atrás cairia no mês corrente e mostraria zero. Botão
|
||
**"Operação inteira"** volta ao recorte natural.
|
||
|
||
**Financeiro — linha "N notas fiscais"** no card Total Entregas, ao lado de "visitas atendidas".
|
||
|
||
**Financeiro — card "N pendentes" agora é clicável** → `/dashboard/pendentes`, a tela nova
|
||
**"Entregas em aberto"**. O número existia desde sempre e **nenhuma tela listava as linhas por trás
|
||
dele** — só dava para descobrir por `rails runner`. A lista traz NF, status, motorista, veículo,
|
||
unidade, data planejada, **em qual operação a NF está** (ou "fora da operação") e **quantas visitas
|
||
o rastreio tem para ela**. Essas duas últimas colunas respondem sozinhas por que a entrega ficou em
|
||
aberto e por que o dashboard de Operações não a mostrava.
|
||
|
||
### 🔍 O que o dado real mostrou (24/08/2026, teste)
|
||
A "1 pendente" que não aparecia em Operações era a **NF 85382 — MARIA APARECIDA JESUS SANTOS**:
|
||
plano avulso, status `pending`, **sem motorista**, planejada 03/08/2026, STS VILA PRUDENTE _
|
||
SAPOPEMBA. Nota **fora da operação** — o `INNER JOIN` a descartava. Confirmado no painel novo.
|
||
|
||
⚠️ **`title` NÃO é o nome do plano.** No dado real ele traz `NF 89096 - KAIQUE TAUAN DA SILVA` — o
|
||
formato da coluna A da planilha de importação (`NF {nota_fiscal} - {nome_completo}`), ou seja, o
|
||
**destinatário**. Agrupar por ele dava um grupo por NF. `COLUNAS_PLANO` passou a ser
|
||
`notes, comments, route_id`; `title` virou a coluna "Destinatário". Quando nenhuma coluna de plano
|
||
vem preenchida, a quebra cai para **unidade**, que ainda informa algo. **A coluna que carrega
|
||
"(Avulsa)"/"INCLUSÃO" segue não confirmada** — pode ser que o espelho simplesmente não a traga
|
||
(o sync já deixa 4 colunas 100% NULL).
|
||
|
||
### ⚙️ Pontos não-óbvios
|
||
|
||
**Duas camadas no mesmo objeto.** `OperacaoMetricas#visitas` = uma linha por ida; `#linhas` = uma
|
||
linha por NF (a última visita). Todos os KPIs, o donut, os motivos, o mapa e a tabela espelho
|
||
continuam saindo de `#linhas` — ou seja, **a tela segue batendo com a planilha do cliente**. Só a
|
||
faixa nova lê `#visitas`.
|
||
|
||
**`uniq` por `tracking_id`.** Sem a CTE, se a mesma `nota_fiscal` estiver repetida dentro de uma
|
||
tabela de operação (ou em duas tabelas do UNION global), o `INNER JOIN` devolvia a **mesma visita**
|
||
mais de uma vez. Agora colapsa.
|
||
|
||
**Filtro de conta unificado.** `Entrega.condicao_conta_sql` nasceu para as queries cruas de
|
||
`Analytics` usarem exatamente o mesmo recorte de `DB_EXISTING_ACCOUNT_ID` do scope
|
||
`da_conta_gade` — antes o dashboard filtrava conta e Operações não.
|
||
|
||
**`NOT IN` com `NULL` devolve zero linhas.** Cada `SELECT` da união em `NotasForaOperacao` filtra
|
||
`nota_fiscal IS NOT NULL`; sem isso um único NULL numa planilha deixaria o painel vazio para sempre.
|
||
Tem spec para isso.
|
||
|
||
### 📂 Arquivos
|
||
```
|
||
app/models/entrega.rb (contas_gade + condicao_conta_sql)
|
||
app/models/operacao.rb (por_notas — em que operação cada NF está)
|
||
app/services/analytics/operacao_metricas.rb (visitas x linhas; dedup pós-filtro; conta)
|
||
app/services/analytics/notas_fora_operacao.rb (NOVO — avulsas/inclusão)
|
||
app/controllers/operacoes_dashboard_controller.rb (período no modo Operação; @fora_operacao)
|
||
app/controllers/dashboard_controller.rb (@notas_atendidas + action #pendentes)
|
||
app/views/dashboard/pendentes.html.erb (NOVO — tela "Entregas em aberto")
|
||
config/routes.rb (GET /dashboard/pendentes)
|
||
app/views/operacoes_dashboard/_painel.html.erb (faixa "Visitas ao local")
|
||
app/views/operacoes_dashboard/_fora_operacao.html.erb (NOVO — painel de avulsas)
|
||
app/views/operacoes_dashboard/index.html.erb (seletor de período + render do painel)
|
||
app/views/dashboard/index.html.erb (linha "N notas fiscais" + card clicável)
|
||
spec/services/analytics/operacao_metricas_spec.rb (+ NF com retentativa)
|
||
spec/services/analytics/notas_fora_operacao_spec.rb (NOVO)
|
||
spec/requests/dashboard_spec.rb (+ tela de entregas em aberto)
|
||
```
|
||
|
||
> **Sem migration e sem gem nova** — model, services, controllers, views e specs.
|
||
|
||
### ⏳ Pendente — roteiro
|
||
```bash
|
||
# 1. Suíte (não pôde ser executada aqui — sem Ruby/Bundler local)
|
||
docker compose exec app bundle exec rspec \
|
||
spec/services/analytics/operacao_metricas_spec.rb \
|
||
spec/services/analytics/notas_fora_operacao_spec.rb \
|
||
spec/requests/dashboard_spec.rb \
|
||
spec/models/entrega_spec.rb
|
||
|
||
# 2. Achar a coluna do PLANO ("(Avulsa)", "INCLUSÃO"). title JÁ foi descartado
|
||
# (é o destinatário). Dump de uma nota avulsa real para ver onde o plano está:
|
||
docker compose exec app bin/rails runner '
|
||
e = Entrega.por_nf(85382).first
|
||
e&.attributes&.reject { |_, v| v.blank? }&.each { |k, v| puts "#{k.ljust(28)} #{v}" }
|
||
'
|
||
|
||
# 3. Com dado real, no mesmo período nas duas telas:
|
||
# financeiro "N notas fiscais" == Operações Global "Total de Entregas"
|
||
# financeiro "Total Entregas" == Operações "Visitas ao local" + notas fora da operação
|
||
```
|
||
|
||
</details>
|
||
|
||
---
|
||
|
||
<details>
|
||
<summary><strong>📣 Notificações: WhatsApp por QR (Baileys) no lugar do Twilio + contatos, grupos e eventos — ETAPA 1 (24/08/2026)</strong></summary>
|
||
|
||
> ⚠️ **STATUS: implementado, NADA executado.** Não há Ruby/Bundler, Postgres nem Docker rodando na
|
||
> máquina de desenvolvimento. Foi conferida a sintaxe de todos os `.rb`/`.erb` (com um checker que
|
||
> emula o handler ERB do Rails, porque `<%= form_with … do %>` não passa no ERB da stdlib) e do
|
||
> `server.js` (`node --check`). **Migration, `npm install`, build do container, pareamento do QR e a
|
||
> suíte continuam pendentes** — roteiro no fim.
|
||
|
||
> Esta é a **etapa 1 de 3**. Ver "O que NÃO está aqui" no fim.
|
||
|
||
### 🎯 O problema
|
||
O canal de WhatsApp era Twilio (pago). Além disso, "quem recebe" era implícito: o
|
||
`NotificacaoService` procurava o `User` motorista **pelo nome** da consolidação. Quem não é usuário
|
||
do sistema — diretoria, cliente, terceiro — não tinha como ser avisado, e não havia tela nenhuma
|
||
para controlar isso. O corpo da mensagem era string interpolada em Ruby.
|
||
|
||
### 🆕 O que existe agora
|
||
|
||
| Tela | O quê |
|
||
|---|---|
|
||
| **Notificações → Contatos** | Cadastro manual: nome, WhatsApp e/ou e-mail, grupo. Quem tem só número recebe só WhatsApp. |
|
||
| **Notificações → Grupos** | Diretoria, Operação, Motoristas… É o **grupo** que assina os eventos. |
|
||
| **Notificações → Eventos** | Cria eventos e marca quais grupos recebem, por qual canal. Gatilho `manual` tem botão "disparar agora". |
|
||
| **Notificações → WhatsApp** | Pareamento por **QR code**, status ao vivo, envio de teste, desconectar. |
|
||
| **Notificações → Envios** | Log de tudo que saiu: destinatário, canal, situação e o erro real. |
|
||
|
||
### ⚙️ Pontos não-óbvios
|
||
|
||
**Container Node novo (`whatsapp/`).** Não existe biblioteca Ruby que fale o protocolo do WhatsApp
|
||
Web — é Baileys. A ponte expõe `/status`, `/enviar`, `/logout` e `/health`, protegida por
|
||
`WHATSAPP_TOKEN`. A **porta não é publicada** no compose: só o container do Rails alcança. Publicar
|
||
exporia um endpoint que manda mensagem em nome da empresa.
|
||
|
||
**A sessão precisa de volume.** `whatsapp_auth:/data` — sem ele, cada deploy exige escanear o QR
|
||
de novo.
|
||
|
||
**Envio serializado e com intervalo.** Disparo em rajada é o que mais causa banimento no canal não
|
||
oficial. A fila do Node serializa e o Rails pausa entre mensagens (`whatsapp_intervalo_segundos`,
|
||
nasce em 5s). Como `sleep(5) × 30 contatos` penduraria o Puma por 2min30, o envio roda em
|
||
`NotificacaoJob`, **nunca dentro da requisição**.
|
||
|
||
**`whatsapp_provedor` nasce em `twilio`.** O deploy não muda o comportamento até o ADM parear o QR
|
||
e trocar o provedor na tela. `Notificacao::Whatsapp` é o ponto único que escolhe — trocar
|
||
Twilio ↔ Baileys é um campo, não um `if` espalhado.
|
||
|
||
**Nomes de rota ≠ nomes de controller, de propósito.** `grupos_contato` e `eventos_notificacao`
|
||
têm singular igual ao plural para o Inflector (que não fala português) e o Rails sufixaria o helper
|
||
de index com `_index` — pegadinha silenciosa. As rotas se chamam `grupos`, `eventos`, `envios`.
|
||
Por isso o `form_with` dos grupos passa `url:` explícita: a rota polimórfica de `GrupoContato`
|
||
procuraria `admin_grupo_contato_path`.
|
||
|
||
**O aviso pessoal ao motorista não regrediu.** Ele continua recebendo o e-mail formatado do
|
||
`ConsolidacaoMailer` (não virou texto puro); o que mudou é que o WhatsApp passa pelo provedor
|
||
escolhido e **tudo fica logado**. Os grupos recebem uma cópia via `Despachante`, com
|
||
`envolvido: nil` para o motorista não receber duas vezes.
|
||
|
||
**Log em tabela, não em arquivo.** O envio engole exceção de propósito (um SMTP fora do ar não pode
|
||
travar um fechamento). Com sessão QR — que cai sozinha e exige repareamento — "o motorista
|
||
recebeu?" vira pergunta de rotina, e a resposta precisava sair do `log/production.log`.
|
||
|
||
### ⚠️ O risco, dito na tela
|
||
Conectar por QR usa a porta do WhatsApp Web por engenharia reversa: está **fora dos Termos do
|
||
WhatsApp** e a Meta **pode banir o número** sem aviso. A tela de pareamento diz isso em texto e
|
||
recomenda **chip dedicado**, não o número principal da operação.
|
||
|
||
### 📂 Arquivos
|
||
```
|
||
db/migrate/20260824000001_create_notificacao_contatos.rb (NOVO)
|
||
db/migrate/20260824000002_create_notificacao_eventos.rb (NOVO — semeia os 2 eventos atuais)
|
||
db/migrate/20260824000003_create_notificacao_envios.rb (NOVO)
|
||
db/migrate/20260824000004_add_baileys_to_configuracao_...rb (NOVO)
|
||
whatsapp/{server.js,package.json,Dockerfile} (NOVO — ponte Baileys)
|
||
docker-compose.yml (serviço whatsapp + volume)
|
||
app/models/{grupo_contato,contato,evento_notificacao}.rb (NOVO)
|
||
app/models/{grupo_evento_assinatura,notificacao_envio}.rb (NOVO)
|
||
app/models/configuracao_notificacao.rb (provedor + credenciais Baileys)
|
||
app/services/notificacao/{cliente_whatsapp,whatsapp,despachante}.rb (NOVO)
|
||
app/services/notificacao_service.rb (grupos + provedor + log)
|
||
app/jobs/notificacao_job.rb (NOVO)
|
||
app/mailers/notificacao_mailer.rb + view (NOVO — e-mail genérico)
|
||
app/controllers/admin/{grupos_contato,contatos,eventos_notificacao}_controller.rb (NOVO)
|
||
app/controllers/admin/{whatsapp_sessoes,notificacao_envios}_controller.rb (NOVO)
|
||
app/policies/{contato,grupo_contato,evento_notificacao,notificacao_envio,whatsapp_sessao}_policy.rb (NOVO)
|
||
app/views/admin/{grupos_contato,contatos,eventos_notificacao,whatsapp_sessoes,notificacao_envios}/ (NOVO)
|
||
app/views/layouts/_navbar.html.erb (seção Notificações)
|
||
config/routes.rb + .env.example
|
||
spec/{models,services}/… (NOVO)
|
||
```
|
||
|
||
> **4 migrations e 1 container novo.** Nenhuma gem nova no Gemfile.
|
||
|
||
### ⏳ Pendente — roteiro
|
||
```bash
|
||
# 1. Gerar o token da ponte e colocar no .env do servidor:
|
||
openssl rand -hex 32 # -> WHATSAPP_TOKEN=...
|
||
# e BAILEYS_URL=http://whatsapp:3001
|
||
|
||
# 2. Subir (a 1ª vez baixa o Baileys; leva alguns minutos):
|
||
docker compose up -d --build
|
||
|
||
# 3. Migrar:
|
||
docker compose exec app bin/rails db:migrate
|
||
|
||
# 4. Suíte (não pôde ser executada aqui — sem Ruby/Bundler local):
|
||
docker compose exec app bundle exec rspec \
|
||
spec/models/contato_spec.rb spec/models/evento_notificacao_spec.rb \
|
||
spec/services/notificacao/
|
||
|
||
# 5. Parear: Notificações → WhatsApp → ler o QR com o CHIP DEDICADO.
|
||
# Depois: Configurações → Notificações → provedor = Baileys, e enviar um teste.
|
||
|
||
# 6. Cadastrar um grupo, um contato e marcar o grupo nos 2 eventos de sistema.
|
||
# Conferir o resultado em Notificações → Envios.
|
||
```
|
||
|
||
### 🚧 O que NÃO está aqui (etapas 2 e 3)
|
||
- **Editor de blocos** (arrastar cabeçalho / tabela de valores / aviso / botão / rodapé, com preview
|
||
e HTML montado no e-mail). Hoje o texto dos 2 eventos de sistema ainda é o do código, e o disparo
|
||
manual usa um campo de texto.
|
||
- **Gatilhos novos**: `valor_alterado`, `operacao_alterada` e `agendado` já existem como opção no
|
||
cadastro e o `Despachante` os atende — mas **ainda não há código chamando** esses gatilhos, nem o
|
||
job de varredura do agendado. Um evento com esses gatilhos hoje só dispara pelo botão manual.
|
||
|
||
</details>
|
||
|
||
---
|
||
|
||
<details>
|
||
<summary><strong>🧱 Editor de blocos das mensagens — ETAPA 2 (24/08/2026)</strong></summary>
|
||
|
||
> ⚠️ **STATUS: implementado, NADA executado.** Sem Ruby/Bundler/Postgres/Docker na máquina de
|
||
> desenvolvimento. Conferida a sintaxe de todos os `.rb`, de todos os `.erb` (checker que emula o
|
||
> handler do Rails) e **do JavaScript do editor** (`node --check` sobre o `<script>` extraído).
|
||
> **Migration, `npm install` e a suíte continuam pendentes.**
|
||
|
||
### 🎯 O que mudou
|
||
Na etapa 1, o corpo das mensagens ainda era string interpolada em Ruby — mudar uma palavra exigia
|
||
deploy. Agora o ADM monta a mensagem **arrastando blocos**, em
|
||
`Notificações → Eventos → WhatsApp / E-mail`.
|
||
|
||
**Blocos:** cabeçalho, texto, tabela de valores, aviso de mudança, botão/link, divisor, rodapé.
|
||
Arraste da paleta (ou clique), reordene pela alça, remova no ✕.
|
||
|
||
**Variáveis:** clique num campo e depois num chip — `{{contato}}`, `{{valor}}`, `{{entregas}}`,
|
||
`{{o_que_mudou}}`… A lista muda conforme o **gatilho** do evento (`Notificacao::Variaveis`).
|
||
|
||
**Um template por evento E por canal:** o mesmo evento tem uma mensagem de WhatsApp e outra de
|
||
e-mail, montadas separadamente. O ✓ na lista de eventos diz quais já estão montadas.
|
||
|
||
### ⚙️ Pontos não-óbvios
|
||
|
||
**O preview roda no servidor.** O botão chama `POST …/template/:canal/preview`, que instancia o
|
||
mesmo `Notificacao::Renderizador` do envio. Uma segunda implementação em JavaScript ficaria mais
|
||
rápida e **inevitavelmente divergiria do que é enviado** — e o preview existe justamente para
|
||
prometer o contrário. O preview do e-mail é exibido num `<iframe sandbox>` para os estilos do
|
||
e-mail não vazarem para o admin.
|
||
|
||
**Duas saídas do MESMO template.** `#texto` produz o WhatsApp (com `*negrito*` e `•` nas tabelas);
|
||
`#html` produz o e-mail com **estilo inline**, porque cliente de e-mail não lê CSS externo. Não usei
|
||
`simple_format` no e-mail: ele gera HTML sem os estilos que o Outlook/Gmail precisam.
|
||
|
||
**Escape em tudo, sempre.** Todo texto do editor e todo valor de variável passa por
|
||
`ERB::Util.html_escape` no caminho HTML. O corpo é digitado numa tela e o preview usa o mesmo
|
||
renderizador — um escape faltando atingiria **primeiro o próprio admin**. Botão só aceita `http(s)`:
|
||
um `javascript:` no href seria clique armado dentro do e-mail. Tem spec para os dois.
|
||
|
||
**Blocos são sanitizados na gravação.** O estado do editor viaja num campo hidden preenchido por
|
||
JavaScript — ou seja, vem de fora. `MensagemTemplate#normalizar_blocos` descarta tipo fora do
|
||
catálogo, campo que aquele tipo não tem e o que não for Hash, e limita a 40 blocos / 20 linhas por
|
||
tabela.
|
||
|
||
**A renderização acontece por destinatário.** `{{contato}}` é o primeiro nome de quem recebe — um
|
||
render compartilhado mandaria o nome da primeira pessoa para todo mundo. Renderizar é manipulação
|
||
de string; o custo por destinatário é irrelevante perto do envio.
|
||
|
||
**Fallback preservado.** Sem template montado (ou com template ativo porém **vazio**), o evento
|
||
continua usando o texto padrão do código. Sem isso, ligar o editor apagaria as notificações que já
|
||
funcionavam.
|
||
|
||
**Amostra ≠ real.** `Variaveis.amostra` só alimenta o preview; `Variaveis.comuns_reais` é o que
|
||
entra num envio de verdade. Trocar os dois colocaria uma data fixa dentro da mensagem que o contato
|
||
recebe — foi um bug que existiu por alguns minutos no disparo manual e está fixado aqui.
|
||
|
||
**`jsonb`, não tabela filha.** A ordem faz parte do dado (é lista, não conjunto), cada tipo de bloco
|
||
tem campos diferentes, e salvar o template inteiro numa transação evita estado meio-salvo.
|
||
|
||
### 📂 Arquivos
|
||
```
|
||
db/migrate/20260824000005_create_mensagem_templates.rb (NOVO)
|
||
app/models/mensagem_template.rb (NOVO — sanitização dos blocos)
|
||
app/models/evento_notificacao.rb (#template, #template_utilizavel)
|
||
app/services/notificacao/blocos.rb (NOVO — catálogo, fonte única)
|
||
app/services/notificacao/variaveis.rb (NOVO — por gatilho + amostra)
|
||
app/services/notificacao/renderizador.rb (NOVO — texto + html)
|
||
app/services/notificacao/despachante.rb (render por canal e por destinatário)
|
||
app/services/notificacao_service.rb (passa as variáveis do fechamento)
|
||
app/jobs/notificacao_job.rb (carrega os dados)
|
||
app/mailers/notificacao_mailer.rb + view (corpo em HTML x texto)
|
||
app/controllers/admin/mensagem_templates_controller.rb (NOVO — edit/update/preview)
|
||
app/views/admin/mensagem_templates/edit.html.erb (NOVO — editor + SortableJS)
|
||
app/views/admin/eventos_notificacao/index.html.erb (links por canal + ✓)
|
||
config/routes.rb
|
||
spec/models/mensagem_template_spec.rb (NOVO)
|
||
spec/services/notificacao/renderizador_spec.rb (NOVO)
|
||
spec/services/notificacao/despachante_spec.rb (+ template)
|
||
```
|
||
|
||
> **1 migration.** Nenhuma gem nova; o SortableJS vem de CDN, como flatpickr/Chart.js/Leaflet.
|
||
|
||
### ⏳ Pendente
|
||
```bash
|
||
docker compose exec app bin/rails db:migrate
|
||
docker compose exec app bundle exec rspec spec/services/notificacao/ spec/models/mensagem_template_spec.rb
|
||
|
||
# Depois: Notificações → Eventos → "WhatsApp" num evento → arrastar blocos →
|
||
# "Atualizar preview" → Salvar. Conferir o corpo real em Notificações → Envios.
|
||
```
|
||
|
||
### 🚧 O que falta (etapa 3)
|
||
`valor_alterado`, `operacao_alterada` e `agendado` existem como opção de gatilho, o editor já
|
||
oferece as variáveis certas para cada um e o `Despachante` os atende — mas **nada no código chama
|
||
esses gatilhos ainda**, nem existe o job que varre os agendados vencidos. Um evento com esses
|
||
gatilhos hoje só dispara pelo botão manual.
|
||
|
||
</details>
|
||
|
||
---
|
||
|
||
<details>
|
||
<summary><strong>🔔 Gatilhos: valor alterado, operação mudou e resumo agendado — ETAPA 3 (24/08/2026)</strong></summary>
|
||
|
||
> ⚠️ **STATUS: implementado, NADA executado.** Sem Ruby/Bundler/Postgres/Docker na máquina de
|
||
> desenvolvimento. Sintaxe conferida em todos os `.rb`, `.rake` e `.erb`. **2 migrations, o
|
||
> `whenever --update-crontab` e a suíte continuam pendentes.**
|
||
|
||
### 🎯 O que faltava
|
||
Nas etapas 1 e 2, `valor_alterado`, `operacao_alterada` e `agendado` existiam como opção de gatilho
|
||
e o editor já oferecia as variáveis certas de cada um — mas **nada no código os chamava**. Um evento
|
||
com esses gatilhos só disparava pelo botão manual.
|
||
|
||
### 🆕 Os três gatilhos, ligados
|
||
|
||
**`valor_alterado`** → `Consolidacao#recalcular_motorista!`. É o **funil único**: todo caminho que
|
||
mexe em pilar, desconto ou lançamento termina ali. Só dispara com a consolidação **finalizada** — em
|
||
rascunho o valor muda a cada clique do wizard, e avisar ali seria spam, não informação. Também só
|
||
dispara se o valor **realmente mudou**. `{{o_que_mudou}}` sai como
|
||
"Valor passou de R$ 4.900,00 para R$ 5.060,00". O próprio motorista entra como *envolvido*, sob o
|
||
`notificar_envolvido` do evento — dá para desligar sem perder o aviso à diretoria.
|
||
|
||
**`operacao_alterada`** → dois caminhos, de propósito:
|
||
- **Imediato**: `Admin::EdicaoLancamentosController#atualizar`. Sabe exatamente qual NF e o que
|
||
mudou (`status: pending → completed`).
|
||
- **Varredura**: `DetectarMudancasOperacaoJob` compara os números de cada operação com o retrato
|
||
anterior (`operacao_snapshots`) e avisa "3 notas na operação a mais, 1 em aberto a menos".
|
||
|
||
Por que os dois: a **maioria** das mudanças da operação não passa pelo nosso código — acontece no
|
||
SimpliRoute e chega pelo sync do espelho, que é read-only aqui. Só o hook interno cobriria a
|
||
minoria dos casos.
|
||
|
||
**`agendado`** → `DispararEventosAgendadosJob`, de hora em hora pelo cron. Os números vêm de
|
||
`Analytics::ResumoOperacao`, que **reusa** `OperacaoMetricas` e `NotasForaOperacao` — o resumo que
|
||
chega no WhatsApp não pode dizer coisa diferente do dashboard.
|
||
|
||
### ⚙️ Pontos não-óbvios
|
||
|
||
**Varredura horária, não uma linha de cron por evento.** Hora e frequência são escolhidas na TELA e
|
||
mudam a qualquer momento; reler o crontab a cada edição acoplaria a UI ao cron do sistema.
|
||
`EventoNotificacao#vencido?` + `ultimo_disparo_em` garantem **um disparo por dia**.
|
||
|
||
**Janelas FECHADAS.** Diária = o dia anterior; semanal = os 7 dias anteriores. Um resumo sobre "hoje"
|
||
mudaria de número depois de enviado.
|
||
|
||
**Período sem movimento não vira mensagem** — mas marca `ultimo_disparo_em` assim mesmo, senão a
|
||
varredura tentaria de hora em hora até o dia virar. Resumo zerado num feriado só treina o leitor a
|
||
ignorar a mensagem.
|
||
|
||
**A primeira varredura de cada operação só grava o retrato.** Sem retrato anterior, toda operação
|
||
existente pareceria "nova" e o primeiro deploy dispararia uma mensagem por operação cadastrada.
|
||
|
||
**Os eventos nascem ativos e isso é seguro.** A migration cria "Valor alterado no fechamento" e
|
||
"Dados da operação mudaram" como eventos de sistema — sem grupo assinante não há destinatário, então
|
||
nada sai até o ADM marcar um grupo. Sem esse seed, `disparar(chave: 'valor_alterado')` não acharia
|
||
evento nenhum e o gatilho ficaria mudo.
|
||
|
||
**Nada nos gatilhos levanta.** Uma notificação não pode derrubar um fechamento nem uma edição de
|
||
lançamento — `Notificacao::Gatilhos` engole e loga, como o resto do módulo.
|
||
|
||
**Custo conhecido da varredura:** `OperacaoMetricas` carrega as visitas em memória para deduplicar
|
||
por NF, e o job varre todas as tabelas 8×/dia. É o preço de reusar a contagem do dashboard em vez de
|
||
escrever um SQL paralelo que divergiria dele. Se incomodar, o caminho é restringir às operações do
|
||
mês corrente e do anterior — as antigas não mudam mais. Está anotado no código.
|
||
|
||
### 📂 Arquivos
|
||
```
|
||
db/migrate/20260824000006_create_operacao_snapshots.rb (NOVO)
|
||
db/migrate/20260824000007_semear_eventos_de_gatilho.rb (NOVO — idempotente)
|
||
app/models/operacao_snapshot.rb (NOVO — retrato + diferença legível)
|
||
app/models/consolidacao.rb (hook em recalcular_motorista!)
|
||
app/services/notificacao/gatilhos.rb (NOVO — os 3 gatilhos)
|
||
app/services/analytics/resumo_operacao.rb (NOVO — reusa as métricas do dashboard)
|
||
app/jobs/disparar_eventos_agendados_job.rb (NOVO)
|
||
app/jobs/detectar_mudancas_operacao_job.rb (NOVO)
|
||
app/controllers/admin/edicao_lancamentos_controller.rb (dispara operacao_alterada)
|
||
app/services/notificacao/despachante.rb (dono do ultimo_disparo_em saiu daqui)
|
||
lib/tasks/notificacao.rake + config/schedule.rb (cron)
|
||
spec/services/notificacao/gatilhos_spec.rb (NOVO)
|
||
spec/jobs/*_spec.rb (NOVO)
|
||
```
|
||
|
||
### ⏳ Pendente
|
||
```bash
|
||
docker compose exec app bin/rails db:migrate
|
||
# O cron do container já roda `whenever --update-crontab` no boot; num container
|
||
# em pé, forçar:
|
||
docker compose exec app bundle exec whenever --update-crontab
|
||
docker compose restart app
|
||
|
||
docker compose exec app bundle exec rspec spec/jobs spec/services/notificacao spec/models
|
||
|
||
# Teste manual: criar um evento "Resumo diário" (gatilho Agendado, hora = agora),
|
||
# marcar um grupo, e rodar à mão:
|
||
docker compose exec app bundle exec rake notificacao:agendados
|
||
docker compose exec app bundle exec rake notificacao:mudancas_operacao
|
||
```
|
||
|
||
</details>
|