Files
Reem-Notas/README.md

1065 lines
62 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 🚛 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 open>
<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 (7681023px): 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>