Files
Reem-Notas/README.md

619 lines
35 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
---
## 📋 CONTEXTO DO PROJETO (leia antes de tudo)
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.
---
## 🏗️ Estado atual — O que já foi feito (Fase 1 ✅)
```
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
```
---
## 🚀 Setup do zero (próximo dev)
### 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
```
---
## 🔧 Comandos do dia a dia
```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
```
---
## 🗺️ Fases de implementação
| 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 + holerite com QR code + modal preview |
| **8** | ✅ Concluída | Painel motorista + login PIN/QR + notificações WhatsApp/email |
**🎉 PROJETO: 8 de 8 fases implementadas.**
---
## 🔧 Análise de integração (11/06/2026) — Correções aplicadas
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 holerite). 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/holerite_pdf.rb # estilo contracheque + QR Code de login rápido
app/controllers/consolidacoes_controller.rb # ATUALIZADO: gerar_pdf_relatorio/gerar_pdf_holerite/preview_holerite
app/views/consolidacoes/show.html.erb # botões por motorista + modal de preview
app/views/consolidacoes/preview_holerite.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 holerite
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 holerite → entra direto sem digitar nada
- Vê 2 cards: 🟧 valor estimado do mês ("aproximado") e ⬜ valor fechado para pagamento
- Baixa o próprio holerite 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 Holerite PDF
6. Logout → `/motorista/login` → entrar com PIN do motorista
7. Conferir cards de valores → baixar próprio holerite → 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.
---
## 🧪 Auditoria + Suíte de testes (15/06/2026)
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.
---
## 🔧 Correções + funcionalidades (16/06/2026)
### 🔴 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
```
---
## 🔧 Correções + funcionalidades (16/06/2026 — sessão 2)
### 🔴 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 holerite** ("sem permissão"): `ConsolidacaoPolicy#show?` exige `pode_consolidar?` (falso para motorista). Novo `autorizar_holerite!` no `ConsolidacoesController` — staff baixa qualquer um; motorista baixa só o próprio (PDF já filtrado pelo nome dele).
- **QR code do holerite 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 holerite (`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
```
---
## 🗄️ Banco existente — referência rápida
| 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`
---
## 🎨 Design — padrão da marca
| 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
---
## 👥 Perfis de acesso
| 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 |
---
## 🩺 Correção (11/06): container caía mostrando ajuda do "rails new"
**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!
```
## 🔐 Segurança
- **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
---
## 🆕 Atualização 17/06/2026 — Pagamentos, Operações, Dashboard financeiro e UI
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 **holerite**.
- **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 Holerite)
- **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.
- **Holerite**: 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`
---
## 🔧 Correções — Holerite PDF (18/06/2026)
### 🔴 Bugs corrigidos
- **500 ao gerar holerite** (`gerar_pdf_holerite`): `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
(holerite 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 holerite 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 do holerite compactados (selo, tabelas, caixa
do total, bloco do QR menor) + folga antes das assinaturas reduzida (`BasePdf#assinaturas`,
60 → 36) — **o holerite agora cabe em uma única página**.
### 📌 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.