# 🚛 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>

