O bundle fica no volume nomeado bundle_cache, que sombreia os gems da imagem — adicionar uma gem (ex.: caxlsx) causava Bundler::GemNotFound ao subir, mesmo com --build, pois o volume persistente não recebe os gems novos. Rodar bundle install no boot resolve de forma idempotente. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
🚛 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.
📋 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,DELETEou 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
git clone https://git.xenserver.com.br/Cludio-code/logistica-controle-custos.git
cd logistica-controle-custos
2. Configure o .env
cp .env.example .env
nano .env # preencha DB_HOST, DB_PASSWORD, etc.
3. Gere o SECRET_KEY_BASE
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
docker-compose up -d
5. Banco + seeds
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
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 + extrato 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
ativoque já existia → erro de coluna duplicada - Criava
pin_acessoduplicando opin_codeexistente - ✅ 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_codeem 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 paraapp/views/admin/+ paths atualizados (admin_usuarios_pathetc.)
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_mese métodomapa_de_precosrestaurados nos models, junto comtipo_label/iconeusados 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::AuditoriaLogsControllertinha rota mas não existia → ✅ criado com view de listagem e filtrostoggle_temaetoggle_ativoexistiam nos controllers mas sem rota → ✅ rotas adicionadas- Controller de configurações usava campo
tipoque não existe (schema usachave) → ✅ corrigido
🟡 Duplicações removidas (arquivos órfãos)
app/controllers/motorista_controller.rb+app/views/motorista/index.html.erb— a rota/motoristausaMotorista::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'ANDcheckout 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:
- WhatsApp: preencha
TWILIO_ACCOUNT_SID,TWILIO_AUTH_TOKENeTWILIO_WHATSAPP_FROMno.enve mude a configuraçãonotificacao_whatsappparatrueno sistema. Motorista precisa tertelefonecadastrado (formato+5511999998888). - Email: preencha as variáveis
SMTP_*no.enve mudenotificacao_emailparatrue. Motorista precisa teremail. - 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
- Login admin → criar consolidação (nome + período + motoristas)
- Wizard Passo 1 → escolher motorista → Passo 2: classificar entregas com os toggles
- Usar "Marcar todos como Normal" → Passo 3: revisar → próximo motorista
- Com tudo classificado → Finalizar (motoristas notificados)
- Na tela da consolidação → "Ver antes de gerar" → Confirmar → baixar Extrato PDF
- Logout →
/motorista/login→ entrar com PIN do motorista - Conferir cards de valores → baixar próprio extrato → sair
▶️ Próximos passos (deploy)
O projeto está completo. Para colocar em produção:
# 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_massaaceitavamtracking_id/motoristaarbitrá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:
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_2026stubamEntrega— 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 usavamethod: :delete(rails-ujs), ignorado pelo Turbo. Agora usadata-turbo-method. - /admin/usuarios e /admin/configuracoes estouravam
NoMethodError: admin_ou_gerente?— helpers de papel (admin?,admin_ou_gerente?) movidos para aApplicationPolicy. - Listagem de usuários chamava o helper inexistente
badge_role— criado emApplicationHelper. - Nova consolidação estourava
NoMethodError: new?—ApplicationPolicyagora definenew? = create?eedit? = update?. - Login do motorista (PIN) derrubava
/motoristacomPG::UndefinedTable: relation "consolidacao_motorista" does not exist— o inflector pt-BR resolvia o composto no singular.self.table_namefixado explicitamente emConsolidacao,ConsolidacaoMotorista,ConsolidacaoEntregaeConfiguracao. - Listagem de usuários faltava o helper
badge_status_usuarioe a rotatoggle_ativoestava sem o prefixoadmin_(verbo errado também) — corrigidos. - Passo 3 (revisão) da consolidação ficava em branco:
GROUP BY tipocombinado comORDER BY created_até inválido no PostgreSQL (PG::GroupingError, vira 500/tela branca em produção). Corrigido comreorder(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.pagasagora éstatus='completed' AND checkout IS NOT NULL(antes eracheckin). 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 trocaconsolidacoes.route_idsporvehicle_ids. - Filtro de conta configurável:
account_idna tabela externa é texto e vinha vazio nos registros recentes (o dado de junho estava 100% em conta vazia, e o filtro fixo em95907zerava o dashboard).DB_EXISTING_ACCOUNT_IDagora aceita lista de contas ouall/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 rodarails db:prepareantes do servidor — não precisa mais rodardb:migratena mão após oup.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).
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. Use95907,(vírgula no fim) para incluir também registros com conta vazia/NULL. O scopeEntrega.da_conta_gadeagora incluiNULLquando 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-cachena página e o gráfico Chart.js virou idempotente (Chart.getChart(ctx)?.destroy()). - 500 ao criar motorista:
users.emaileraNOT NULLcom índice único e default""— o 2º motorista sem e-mail colidia em""(RecordNotUnique). Agora a coluna permiteNULLe e-mail em branco viranil(User#normalizar_email_em_branco); no Postgres váriosNULLconvivem no índice único. - 500 ao gerar relatório PDF (
gerar_pdf_relatorio):@entregas.group(:tipo).countcom.order(:created_at)geravaGROUP BY ... ORDER BY created_at→PG::GroupingError. Corrigido comreorder(nil). Também removidas as opçõescolor:passadas aoPrawn#text(não suportadas — uso defill_color). - Motorista não baixava o próprio extrato ("sem permissão"):
ConsolidacaoPolicy#show?exigepode_consolidar?(falso para motorista). Novoautorizar_extrato!noConsolidacoesController— 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). Novourl_acessorespeita o esquema doAPP_HOST(sem esquema = http). DefinaAPP_HOSTcom 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_massaaceitaacao: 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.emailpassa a permitirNULL; e-mails""existentes viramNULL.
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)
# 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.keyou 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: colunaspago_em,pago_por,forma_pagamentoemconsolidacao_motoristas(pagamento é por motorista). ConsolidacaoMotorista:pago?,marcar_pago!(user, forma:),cancelar_pagamento!, scopespagos/pendentes,belongs_to :pagador.Consolidacao#status_pagamento(:pendente/:parcial/:pago),valor_pago,valor_pendente, scopespagamento_pago/pagamento_pendente/pagamento_parcial(filtro da lista).- Tela show: marcar motorista individual ou a operação inteira (com
<select>de forma); estorno. Açõesregistrar_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-mailConsolidacaoMailer#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 viainformation_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) emconsolidacoes;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(defaultgade_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ícioe< 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_pagese faltar espaço — o Prawn não pagina imagem sozinho).
🎨 UI / Identidade visual
- Fonte base maior para leitura:
html { font-size: 17px }(desktop) e18px(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]), commix-blend-mode: screenpara "derrubar" o fundo preto do vídeo. rack-mini-profilerdesativado emconfig/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(verOperacao). - 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) ebadge_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 — Extrato PDF (18/06/2026)
🔴 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:BasePdfagora 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.ttfeDejaVuSans-Bold.ttf(a imagem Dockerruby:3.2.2-slimnão traz fontes do sistema; oCOPY . .do Dockerfile e o volume.:/appgarantem que estejam disponíveis em runtime).
- Fontes versionadas em
-
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_downera frágil e, com a fonte nova, fazia o QR se sobrepor ao texto/assinaturas. ✅ Corrigido:desenhar_qrcodeagora 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 (Usermotorista comlogin_token). Sem conta correspondente,user_motoristaficavanile 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) eusers.nome(digitado no admin). ✅ConsolidacoesController#buscar_user_motoristaagora 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.
- Nome não batia (espaços/maiúsculas) entre
📌 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.
🆕 Atualização 19/06/2026 — Apontamento manual / Nota avulsa
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 viada_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 dodriverda 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
driverda 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) emconsolidacao_entregasdistingue apontamento de entrega elegível do período.Consolidacao#classificadas_countignora osmanual: truepara não estourar a barra de progresso nem o gate de finalização (>= 100%). - Lógica centralizada em
Consolidacao#adicionar_apontamento!(entrega, tipos, user)eConsolidacao#recalcular_motorista!— reusadas pelos dois fluxos (wizard e avulsa). Preço por pilar viaConsolidacaoEntrega.valor_para(tipo)(fonte única). Dados da entrega viaEntrega#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) enota_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 dadivdo controller).
🆕 Migration adicionada (rodar db:migrate)
20260619000001_add_manual_to_consolidacao_entregas.rb
docker-compose exec app bundle exec rails db:migrate
🧪 Testes
spec/models/consolidacao_spec.rb:classificadas_countignora apontamentos manuais eadicionar_apontamento!(multi-tipo, idempotente, inclui o motorista e soma o valor).
🆕 Atualização 22/06/2026 — Pilar Extraordinária, Perfil Externo, Falhadas, Relatórios e Segurança QR
🆕 Funcionalidades
- Pilar "Entrega Extraordinária" — preço coringa para entregas fora do planejamento.
- Novo valor
extraordinarianoenum tipodeConsolidacaoEntrega(soma positiva, como Bônus). Sem migration de enum (colunainteger). - 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_classificacaono controller +pedirValor()no Stimulus).
- Novo valor
- 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+ scopefalhadas; o scopependentesfoi ajustado para não sobrepor (nemcompletednem falha).
- Novo
- 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).
- Por consolidaçã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.
- Login normal por e-mail/senha; enxerga apenas o dashboard. Bloqueado nas demais áreas pelas policies Pundit (
- 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 usarupdate_columns(pulam validações e o callback de recálculo) — arquivar é soft-delete administrativo e deve sempre funcionar.turbo_confirmmovido 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) comdateFormat: 'd/m/Y'. Corrigido passando objetosDate+ separador" até "; seleção de um único dia agora aplica; re-inicialização emturbo:load. - Moeda sem separador de milhar — padronizado
R$ 1.234,56(milhar., decimais,) em: helpermoeda(number_to_currency), PDFs (base_pdf),Configuracao#valor_formatado, mensagens doNotificacaoServicee 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)
docker-compose exec app bundle exec rails db:migrate
20260622000001_add_preco_extraordinaria_configuracao— cria a configpreco_extraordinaria(padrãoR$ 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.docker-compose exec app bundle exec rails runner "puts Entrega.da_conta_gade.distinct.pluck(:status).inspect"
🆕 Atualização 25/06/2026 — Consolidação inclui entregas de insucesso (motorista foi ao local)
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) OUfailed(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)+ scopeatendidas(where(status: STATUS_ATENDIDO).com_checkout). Novo helper de instânciaEntrega#falhada?.Entrega.contar_pagas→ renomeado paraEntrega.contar_atendidas(usa o scopeatendidas).Consolidacao#entregas_elegiveis_countpassa 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) usamEntrega.atendidas.- Não afeta o lado financeiro: dashboard, painel do motorista, job de 1h e
HistoricoEstimadocontinuam 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.
🆕 Atualização 25/06/2026 — Apontamento 100% manual + Arquivar motorista individualmente
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_manualquando não háEntregapara 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:)— geratracking_idsintético ("MAN-<uuid>") compartilhado pelos pilares (agrupa como UMA entrega); reusarecalcular_motorista!. Actionapontar_manual+ rota + Stimulusapontamento_manual_controller.js.ConsolidacaoMotorista: scopesativos/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. Actionsarquivar_motorista/desarquivar_motorista(autorizaçãoupdate?; 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
docker-compose exec app bundle exec rails db:migrate
docker-compose exec app bundle exec rspec
🆕 Atualização 25/06/2026 — Fechar por veículo na tela de Validar
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íveise 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 scopeEntrega.da_veiculo. Grupo "sem veículo" via sentinelaSEM_VEICULO.validaraceitaparams[:vehicle]: recorta@entregase, quando há carro selecionado, os totais da barra (@total_entregas/@classificadas) vêm doveiculos_statusdaquele carro.tracking_ids_elegiveis(motorista, vehicle = nil)eclassificar_em_massapassam o veículo — é o que faz "Marcar todos" fechar só o carro.resumo_json(motorista, vehicle:)devolveclassificadas_escopo(barra por carro) +veiculos_status(refresh dos chips).validacao_controller.js: valueveiculo, targetchipVeiculo,vehiclenos 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
🆕 Atualização 26/06/2026 — Entrega de termo, caixa de ferramentas, gestão de motoristas e UX da Validação
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, viaCHAVES_MOEDA). - Lançado como uma linha por lote com a coluna nova
quantidade;valor_aplicadoguarda o total do lote (quantidade × preço), entãorecalcular_motorista!e todas as somas existentes seguem corretas. Modal com stepper − N + (sem as setinhas nativas doinput 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 decountparasum(: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_countseguemanual: 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-sesticky/z-indexpara 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 configpreco_termo(idempotente).
Consolidacao#adicionar_termos!(motorista:, quantidade:, user:)— uma linhaTERMO-<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_tipoagorasum(: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); removidosmarcarTodos/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
docker-compose exec app bundle exec rails db:migrate
🔧 Atualização 29/06/2026 — Responsividade da tela de Validar (telas menores)
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) usavaflex gap-1.5semflex-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. Semflex-wrap, os botões transbordavam por cima do endereço/data/veículo. O ponto de virada foi movido paralg(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-wrapimpede o transbordo;lg:shrink-0mantém os botões inteiros no desktop, deixando o texto da nota truncar em vez de espremer os botões).
- Card da entrega:
- 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-wrape ≥ 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.
🆕 Atualização 29/06/2026 — Dashboard de Operações (análise de entregas + mapa)
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.
- 🏥 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
- 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: CTEROW_NUMBER() OVER (PARTITION BY reference_id ORDER BY checkout DESC)sobredb_reem_simplerout_2026+INNER JOINna tabelagade_entregas_*porreference_id::text = nota_fiscal; agrega tudo em Ruby (visãolinhas= 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 — replicarda_conta_gadezerava tudo quandoDB_EXISTING_ACCOUNT_IDnã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 viramNULLquando não existem — assim oUNION ALLentre operações no Global não quebra (era o errocolumn g.status does not exist). - Segurança: nomes de tabela passam por
Operacao.sanitizar+quote_table_name; afoto_da_fachadaé validada como URLhttp(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_2026read-only egade_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.
🆕 Atualização 30/06/2026 — Mapa, dashboard clicável, validação por veículo e UX da sidebar
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), viaEntrega::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(deEntrega) 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 filtrosf_resultado(colunastatus) ef_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
#ef4444para azul#3b82f6nos 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-2nos 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-collapsedno<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_2026read-only egade_entregas_*). ⚠️ Em produção, reiniciar o Puma após o deploy (cache de classes/views).
🔒 Segurança + auto-atualização do painel + cron (06/07/2026)
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
.envnão está no repositório): definirRAILS_ENV=productioneFORCE_SSL=trueno.enve rebuildar. Ao migrar paraproduction, 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). Emapplication.rbfica atrás deENV["FORCE_SSL"]=="true", então funciona mesmo enquanto o deploy roda fora do modo production;production.rbtambém já vem comforce_ssl = true. - rack-mini-profiler desligado (item 3) — no
Gemfilepassou arequire: false: a gem não monta mais o middleware (fim do cookie__profiline da rota/mini-profiler-resources), em qualquer ambiente. - Content Security Policy (item 5) — nova política em
config/initializers/content_security_policy.rbcom 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) fazTurbo.visitna 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 deevery 1.hourpara*/30 8-18(a cada 30 min, 08h-18h), batendo com a cadência real.Dockerfile— instala o pacotecron.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
# 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.
✏️ Atualização 08/07/2026 — Editar Lançamento do SimpliRoute (correção pelo ADM)
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, modelEntrega) é 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
idnumérico (usado na URL de escrita) etracking_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 pelotracking_id(fallback: NF). - Motivo = campo
checkout_observation, que é o UUID de uma lista fixa de 14 motivos (GET /v1/routes/observations/, todostype=failed). É esse UUID que popula a colunaobservation— 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 peloapplication.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
# 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).