Go to file
2026-07-03 15:18:51 -03:00
2026-07-03 15:18:51 -03:00
2026-06-10 23:37:41 -03:00
2026-06-17 19:23:50 -03:00
2026-06-30 23:50:19 -03:00
2026-06-10 20:12:20 -03:00

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

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 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 inexistentesEntrega.do_mes e Configuracao.mapa_de_precos foram sobrescritos pelo commit das Fases 4-6 → a raiz do site quebrava (root = dashboard#index).

  • Corrigido: scope do_mes e método mapa_de_precos restaurados nos models, junto com tipo_label/icone usados pelas views de configurações

6. AuditoriaLog com 2 assinaturas — Fase 2 chamava registrar(user, 'acao', ip) posicional; o model usa keyword args → ArgumentError em login PIN, CRUD de usuários e configurações.

  • Corrigido: todas as chamadas convertidas para a assinatura do model

7. Rotas/controllers ausentes

  • Admin::AuditoriaLogsController tinha rota mas não existia → criado com view de listagem e filtros
  • toggle_tema e toggle_ativo existiam nos controllers mas sem rota → rotas adicionadas
  • Controller de configurações usava campo tipo que não existe (schema usa chave) → corrigido

🟡 Duplicações removidas (arquivos órfãos)

  • app/controllers/motorista_controller.rb + app/views/motorista/index.html.erb — a rota /motorista usa Motorista::DashboardController (Fase 8)
  • app/views/layouts/_sidebar.html.erb — o layout renderiza _navbar.html.erb
  • Seeds dos 3 motoristas de teste (João/1234, Maria/5678, Carlos/9012) restaurados — tinham sido sobrescritos

⚠️ Observação sobre login PIN duplicado

Existem dois fluxos de login por PIN funcionais: a aba PIN na tela Devise (/auth/login, Fase 2) e a tela dedicada (/motorista/login, Fase 8 — usada pelo QR Code do extrato). Ambos usam pin_code e funcionam; recomenda-se manter os dois (a tela dedicada é melhor para mobile) ou unificar futuramente.

📂 Arquivos das Fases 4, 5 e 6

Fase 4 — Job 1h + Auditoria:

config/schedule.rb                                  # Whenever — roda a cada 1h
lib/tasks/historico.rake                            # rake historico:atualizar
app/jobs/application_job.rb
app/jobs/atualizar_historico_estimado_job.rb        # conta entregas pagas × preco_entrega
app/controllers/api/v1/dashboard_controller.rb      # GET /api/v1/dashboard/metricas
app/controllers/concerns/auditavel.rb               # auditar!(:acao, registro) nos controllers
app/policies/dashboard_policy.rb

Ativar o cron no servidor: bundle exec whenever --update-crontab Testar manualmente: docker-compose exec app bundle exec rake historico:atualizar

Fase 5 — Consolidação + Wizard Passo 1:

db/migrate/20260101000008_add_route_ids_to_consolidacoes.rb   # rodar db:migrate!
app/controllers/consolidacoes_controller.rb         # index/new/create/show/wizard/finalizar/arquivar
app/policies/consolidacao_policy.rb
app/views/consolidacoes/index.html.erb              # lista com filtros (status/nome/período/motorista/rota)
app/views/consolidacoes/new.html.erb                # form: nome + 2 date pickers + multi-select motoristas/rotas
app/views/consolidacoes/wizard.html.erb             # Passo 1: lista motoristas com progresso

Fase 6 — Validação + Toggles + Massa:

app/controllers/consolidacao_entregas_controller.rb # validar/classificar/classificar_em_massa/revisar
app/views/consolidacao_entregas/validar.html.erb    # Passo 2: toggles coloridos + checkbox massa + resumo lateral
app/views/consolidacao_entregas/revisar.html.erb    # Passo 3: resumo + próximo motorista / salvar rascunho
app/javascript/controllers/validacao_controller.js  # Stimulus: tempo real (progresso, resumo, toggles)
config/routes.rb                                    # ATUALIZADO — rotas do wizard

Cores dos toggles (padrão do projeto): 🟧 Laranja = Entrega Normal · 🟫 Laranja escuro = Retirada · Branco = Bônus · Preto = Desconto

Regras implementadas:

  • Entregas elegíveis: status='completed' AND checkout IS NOT NULL (somente leitura da tabela existente)
  • Não permite finalizar consolidação com entregas não classificadas (botão desabilitado + validação server-side)
  • Consolidação finalizada não pode ser editada por operadores (Pundit)
  • Todas ações críticas registradas em auditoria_logs

Fase 7 — PDFs (Prawn + QR Code):

Gemfile                                             # + rqrcode, chunky_png
db/migrate/20260101000009_add_login_token_to_users.rb   # token para QR — rodar db:migrate!
app/services/pdf/base_pdf.rb                        # layout base: cabeçalho preto/laranja + rodapé
app/services/pdf/relatorio_motorista_pdf.rb         # tabela detalhada de entregas + resumo + total
app/services/pdf/extrato_pdf.rb                    # estilo contracheque + QR Code de login rápido
app/controllers/consolidacoes_controller.rb         # ATUALIZADO: gerar_pdf_relatorio/gerar_pdf_extrato/preview_extrato
app/views/consolidacoes/show.html.erb               # botões por motorista + modal de preview
app/views/consolidacoes/preview_extrato.html.erb   # preview HTML antes de confirmar o PDF
app/models/user.rb                                  # ATUALIZADO: regenerar_login_token!

Soft delete já implementado: consolidações finalizadas vão para "arquivada", nunca são excluídas.

Fase 8 — Painel Motorista + Notificações:

Gemfile                                             # + twilio-ruby
app/controllers/motorista/sessoes_controller.rb     # login PIN 4 dígitos + acesso via QR (/motorista/acesso/:token)
app/controllers/motorista/dashboard_controller.rb   # 2 cards: estimado (mês) + consolidado (finalizadas)
app/views/motorista/sessoes/new.html.erb            # teclado numérico gigante mobile-first (envia no 4º dígito)
app/views/motorista/dashboard/index.html.erb        # cards laranja/branco + lista com download do próprio extrato
app/services/notificacao_service.rb                 # WhatsApp (Twilio) + email — falha nunca quebra a finalização
app/mailers/application_mailer.rb
app/mailers/consolidacao_mailer.rb                  # pagamento_fechado
app/views/consolidacao_mailer/pagamento_fechado.html.erb   # email com identidade visual da marca
app/views/layouts/mailer.html.erb
config/initializers/smtp.rb                         # SMTP via ENV (só ativa se SMTP_USERNAME existir)
config/routes.rb                                    # ATUALIZADO: /motorista/login + /motorista/acesso/:token

Notificações — como ativar:

  1. WhatsApp: preencha TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN e TWILIO_WHATSAPP_FROM no .env e mude a configuração notificacao_whatsapp para true no sistema. Motorista precisa ter telefone cadastrado (formato +5511999998888).
  2. Email: preencha as variáveis SMTP_* no .env e mude notificacao_email para true. Motorista precisa ter email.
  3. Ao finalizar uma consolidação, cada motorista recebe automaticamente a mensagem com o valor e o link do painel.

Fluxo do motorista (público de baixo nível técnico):

  • Acessa /motorista/login → digita PIN de 4 números num teclado gigante (envia sozinho no 4º dígito)
  • OU escaneia o QR Code impresso no extrato → entra direto sem digitar nada
  • Vê 2 cards: 🟧 valor estimado do mês ("aproximado") e valor fechado para pagamento
  • Baixa o próprio extrato das consolidações finalizadas

🧪 Teste end-to-end sugerido

  1. Login admin → criar consolidação (nome + período + motoristas)
  2. Wizard Passo 1 → escolher motorista → Passo 2: classificar entregas com os toggles
  3. Usar "Marcar todos como Normal" → Passo 3: revisar → próximo motorista
  4. Com tudo classificado → Finalizar (motoristas notificados)
  5. Na tela da consolidação → "Ver antes de gerar" → Confirmar → baixar Extrato PDF
  6. Logout → /motorista/login → entrar com PIN do motorista
  7. Conferir cards de valores → baixar próprio extrato → sair

▶️ Próximos passos (deploy)

O projeto está completo. Para colocar em produção:

# 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::UsuariosControllertoggle_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 zero20260610_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 GadeDashboardController 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:

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).
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_atPG::GroupingError. Corrigido com reorder(nil). Também removidas as opções color: passadas ao Prawn#text (não suportadas — uso de fill_color).
  • Motorista não baixava o próprio extrato ("sem permissão"): ConsolidacaoPolicy#show? exige pode_consolidar? (falso para motorista). Novo autorizar_extrato! no ConsolidacoesController — staff baixa qualquer um; motorista baixa só o próprio (PDF já filtrado pelo nome dele).
  • QR code do extrato não abria: URL estava fixa em https://, mas o servidor roda em http (IP:porta). Novo url_acesso respeita o esquema do APP_HOST (sem esquema = http). Defina APP_HOST com o IP/host acessível pelo celular.

🆕 Funcionalidades

  • NF + Endereço no relatório PDF: colunas separadas; a coluna Tracking foi substituída por Veículo (relatório e tela de revisão / passo 3).
  • Endereço nas telas da consolidação: linha de endereço nos cards do passo 2 (validação) e coluna Endereço no passo 3 (revisão, paginado 20/50/100).
  • Campos de assinatura nos PDFs: bloco com assinatura do Motorista e do Administrador (Reem Transporte) no relatório e no extrato (BasePdf#assinaturas).
  • Selecionar todos no passo 2: checkbox que marca/desmarca todas as entregas de uma vez.
  • Desselecionar em massa: toggle "Modo remover" — os botões Normal/Retirada/Bônus/Desconto (e "marcar todos") passam a remover o pilar das entregas selecionadas (classificar_em_massa aceita acao: remover).
  • Botões de voltar entre os passos do wizard (2→1, 3→2) viraram botões visíveis (antes eram texto cinza discreto).

🆕 Migration adicionada (rodar db:migrate)

  • 20260616000003_allow_null_email_for_motoristasusers.email passa a permitir NULL; e-mails "" existentes viram NULL.
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.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 extrato.
  • Notificação de pagamento: NotificacaoService.notificar_pagamento (WhatsApp + e-mail ConsolidacaoMailer#pagamento_efetuado) — falhas só logam.
  • Aviso in-app no painel do motorista: faixa "Pagamento confirmado" (últimos 7 dias) + selo PAGO por consolidação.

🗂️ Consolidações por OPERAÇÃO (tabelas externas gade_entregas_*)

  • PORO app/models/operacao.rb: descobre as tabelas via information_schema, com whitelist anti-injection (Operacao.sanitizar + quote_table_name) — nunca interpolar nome de tabela cru. Operacao.todas, dados(tabelas), agrupadas_por_mes.
  • Entrega.da_operacoes(tabelas): junta por NF (reference_id::text = nota_fiscal).
  • Coluna operacoes (jsonb) em consolidacoes; Consolidacao.com_operacoes(ops).
  • Nova consolidação: multi-seleção de operação pré-preenche motoristas/veículos/período e filtra as entregas por NF; liberdade de marcar um ou todos os motoristas da operação.
  • ENV opcional OPERACOES_TABLE_PREFIX (default gade_entregas_).

📊 Dashboard financeiro

  • Filtro de operação no topo (multi-seleção, agrupada por mês, com "Limpar") aplicado a tudo.
  • KPIs: Custo total, Ticket médio/entrega, Pago, A pagar.
  • Gráficos (Chart.js): custo por operação, composição por tipo, custo por motorista, rosca pago × a pagar + tabelas (pagos por data de pagamento, pendentes).

📅 Correção de data — usar checkout (data real), não planned_date

  • Entrega.no_periodo_checkout(inicio, fim): filtro pela data real da conclusão, com comparação naïve (intervalo meio-aberto >= início e < fim+1), sem conversão de fuso — igual à análise feita direto no banco.
  • Aplicado em contar_pagas, elegibilidade do wizard, gráfico principal (DATE(checkout)) e na exibição da data das entregas.

🧾 PDFs (Relatório e Extrato)

  • Relatório: endereço completo com quebra de linha, larguras de coluna fixas, quebra de página antes do RESUMO. Rodapé via canvas (na margem inferior — não sobrepõe mais o conteúdo); margem inferior 72.
  • Extrato: coluna Unit., os 4 pilares sempre exibidos, total de entregas + veículos, selo de pagamento (PAGO/PENDENTE) e garantia de espaço para o QR Code (start_new_page se faltar espaço — o Prawn não pagina imagem sozinho).

🎨 UI / Identidade visual

  • Fonte base maior para leitura: html { font-size: 17px } (desktop) e 18px (mobile).
  • Contraste elevado no dashboard (text-gray-500/600text-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 — 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: BasePdf agora registra a família DejaVu Sans (TTF) e a define como fonte padrão (registrar_fonte_utf8), com suporte total a UTF-8. Vale para todos os PDFs (extrato e relatório) — qualquer caractere Unicode passa a funcionar.

    • Fontes versionadas em app/assets/fonts/DejaVuSans.ttf e DejaVuSans-Bold.ttf (a imagem Docker ruby:3.2.2-slim não traz fontes do sistema; o COPY . . do Dockerfile e o volume .:/app garantem que estejam disponíveis em runtime).
  • Assinaturas do extrato caíam na 2ª página: após o QR Code o cursor ficava abaixo do limite de quebra (start_new_page if cursor < 120), empurrando o bloco de assinaturas para uma página nova. Corrigido: espaçamentos compactados com segurança (padding das tabelas [4,8], QR Code menor) + folga antes das assinaturas reduzida (BasePdf#assinaturas, 60 → 36, limite de quebra 120 → 100) — o extrato agora cabe em uma única página.

  • QR Code sobrepondo / sumindo: o posicionamento por move_up/move_down era frágil e, com a fonte nova, fazia o QR se sobrepor ao texto/assinaturas. Corrigido: desenhar_qrcode agora desenha a imagem com posição absoluta (at:) — que não move o cursor — e avança o cursor manualmente pela altura da imagem. Também garantido que cada caixa (selo de pagamento, total) tenha folga ≥ a própria altura para não sobrepor o conteúdo seguinte.

  • QR Code não aparecia para alguns motoristas: o QR aponta para o painel do motorista (/motorista/acesso/:token), que exige uma conta de usuário (User motorista com login_token). Sem conta correspondente, user_motorista ficava nil e a seção do QR era omitida silenciosamente. Duas causas tratadas:

    • Nome não batia (espaços/maiúsculas) entre consolidacao_motoristas.motorista_nome (dado externo) e users.nome (digitado no admin). ConsolidacoesController#buscar_user_motorista agora tenta o match exato e, se falhar, compara nomes normalizados (sem espaços extras/duplos, case-insensitive).
    • Motorista sem conta: em vez de omitir, o extrato agora desenha um aviso ("Cadastro de acesso ainda não disponível… solicite ao administrador") no lugar do QR (ExtratoPdf#desenhar_aviso_sem_acesso). O QR só existe para motoristas com cadastro no site — comportamento esperado, já que sem conta não há painel para acessar.

📌 Padrão (novo)

  • PDFs com Prawn: usar sempre a fonte TTF registrada no BasePdf (DejaVu, UTF-8). Nunca contar com a fonte embutida — ela quebra em qualquer caractere fora do Windows-1252.

🆕 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ó dela (Entrega.por_nf, sem filtro de período, restrito à conta Gade via da_conta_gade).

🆕 Funcionalidades

  • Apontamento dentro de uma consolidação existente (wizard Passo 1): botão " Apontamento manual" → modal busca a NF → confere os dados (motorista/veículo/data) → escolhe um ou vários pilares (Normal/Retirada/Bônus/Desconto) → adiciona.
  • "Nota avulsa" (tela de Consolidações): botão " Nota avulsa" → busca a NF → ao prosseguir cria já a consolidação (nome Avulsa nf<NF>, período = data da entrega → fim do mês, motorista vindo do driver da entrega) com o apontamento, e abre a tela da consolidação.
  • Múltiplas classificações por apontamento: cria um pilar (ConsolidacaoEntrega) por tipo escolhido, somando os valores (mesmo modelo multi-pilar das entregas normais).
  • Motorista automático: vem do driver da entrega; se ainda não estiver na consolidação, é incluído.
  • Gestão na tela da consolidação (show): seção " Apontamentos manuais" lista NF, motorista, pilares e valor, com botão de remover (a nota inteira). Na revisão (Passo 3) os apontamentos aparecem com o selo " Apontamento" e botão de remover (por pilar).

⚙️ Como funciona (técnico)

  • Coluna manual (boolean, default false) em consolidacao_entregas distingue apontamento de entrega elegível do período. Consolidacao#classificadas_count ignora os manual: true para não estourar a barra de progresso nem o gate de finalização (>= 100%).
  • Lógica centralizada em Consolidacao#adicionar_apontamento!(entrega, tipos, user) e Consolidacao#recalcular_motorista! — reusadas pelos dois fluxos (wizard e avulsa). Preço por pilar via ConsolidacaoEntrega.valor_para(tipo) (fonte única). Dados da entrega via Entrega#resumo_apontamento.
  • Rotas novas:
    • consolidacoes (coleção): GET buscar_nf (JSON), POST avulsa.
    • consolidacao_entregas (coleção): GET buscar_nf, POST apontar, DELETE remover_apontamento (aceita ?id= para um pilar ou ?tracking_id=&motorista= para a nota toda).
  • Stimulus: apontamento_controller.js (modal do wizard) e nota_avulsa_controller.js (modal da index). ⚠️ Identificador do Stimulus segue o nome do arquivo em dash-case: nota_avulsa_controller.jsdata-controller="nota-avulsa" / data-action="nota-avulsa#..." (e o elemento da ação precisa estar dentro da div do controller).

🆕 Migration adicionada (rodar db:migrate)

20260619000001_add_manual_to_consolidacao_entregas.rb
docker-compose exec app bundle exec rails db:migrate

🧪 Testes

  • spec/models/consolidacao_spec.rb: classificadas_count ignora apontamentos manuais e adicionar_apontamento! (multi-tipo, idempotente, inclui o motorista e soma o valor).

🆕 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 extraordinaria no enum tipo de ConsolidacaoEntrega (soma positiva, como Bônus). Sem migration de enum (coluna integer).
    • Aparece em todas as telas dos pilares: wizard (passo 1), validação, revisão, nota avulsa, extrato, PDFs e composição do dashboard.
    • Preço padrão configurável em Configurações (preco_extraordinaria) e valor customizável por entrega: ao marcar "Extra" no wizard abre um prompt já preenchido com o padrão (valor_classificacao no controller + pedirValor() no Stimulus).
  • Card "Consolidado / Pago" no topo do dashboard (total consolidado + total já pago do período; reusa @fin_custo_total / @fin_pago).
  • Entregas falhadas no dashboard — o card "Total Entregas" agora separa entregues · pendentes · falhadas.
    • Novo Entrega::STATUS_FALHA + scope falhadas; o scope pendentes foi ajustado para não sobrepor (nem completed nem falha).
  • Relatórios financeiros em PDF (admin):
    • Por consolidaçãoPdf::RelatorioFinanceiroConsolidacaoPdf (botão "📊 Relatório financeiro" na tela da consolidação).
    • Por períodoPdf::RelatorioFinanceiroPeriodoPdf (botão "🖨️ Relatório" no dashboard, respeita o filtro de período/operação).
  • Novo perfil de usuário: "Externo (só dashboard)" (role: externo).
    • Login normal por e-mail/senha; enxerga apenas o dashboard. Bloqueado nas demais áreas pelas policies Pundit (pode_consolidar?/admin). Ideal para enviar a outras gerências visualizarem os indicadores.
  • Filtro de data re-estilizado — calendário no tema escuro (flatpickr dark + acento laranja) e atalhos rápidos: Hoje / 7 dias / Este mês / 30 dias.

🔴 Bugs corrigidos

  • Botão "Arquivar" não funcionava em consolidação zerada / rascunho. Consolidacao#arquivar! e #reativar! passaram a usar update_columns (pulam validações e o callback de recálculo) — arquivar é soft-delete administrativo e deve sempre funcionar. turbo_confirm movido para o <form> (mais confiável).
  • Caixa do filtro de data mostrava uma data solta em vez do período. Causa: o flatpickr recebia datas em ISO (YYYY-MM-DD) com dateFormat: 'd/m/Y'. Corrigido passando objetos Date + separador " até "; seleção de um único dia agora aplica; re-inicialização em turbo:load.
  • Moeda sem separador de milhar — padronizado R$ 1.234,56 (milhar ., decimais ,) em: helper moeda (number_to_currency), PDFs (base_pdf), Configuracao#valor_formatado, mensagens do NotificacaoService e tooltips/JS do dashboard e do wizard.
  • R$ "quebrado" no painel do motorista (celular) — os valores grandes quebravam "R$" e o número em linhas separadas. Adicionado whitespace-nowrap + fonte responsiva (text-4xl sm:text-5xl) nos cards.

🔐 Segurança

  • QR Code do extrato não loga mais direto. Antes, ler o QR (/motorista/acesso/:token) autenticava o motorista sem PIN. Agora o QR apenas identifica o motorista e leva à tela de PIN; ele precisa digitar o PIN de 4 dígitos para entrar — e o PIN tem que ser do mesmo motorista do QR (impede usar o QR de outra pessoa). A tela saúda pelo nome quando vem do QR.

🆕 Migration adicionada (rodar db:migrate)

docker-compose exec app bundle exec rails db:migrate
  • 20260622000001_add_preco_extraordinaria_configuracao — cria a config preco_extraordinaria (padrão R$ 25,00) em bancos já existentes (idempotente; não sobrescreve valor já definido).

Confirmado (25/06/2026)

  • O status de falha gravado pelo SimpleRoute é exatamente failedEntrega::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) OU failed (insucesso), sempre com checkout registrado. Substitui o antigo critério de só .pagas (completed) na consolidação.
  • Status visível em cada entrega na tela de validação (Passo 2): selo ✓ Entregue (verde) ou ⚠️ Insucesso (vermelho), com o card de insucesso destacado em borda vermelha — facilita visualmente identificar o que classificar.
  • Insucesso entra para classificação manual, não com valor automático: o admin escolhe o pilar/valor (Normal, Retirada, Bônus, Desconto, Extraordinária) — o motorista foi ao local, então a entrega aparece na lista, na contagem e na barra de progresso.

⚙️ Como funciona (técnico)

  • Entrega::STATUS_ATENDIDO = (%w[completed] + STATUS_FALHA) + scope atendidas (where(status: STATUS_ATENDIDO).com_checkout). Novo helper de instância Entrega#falhada?.
  • Entrega.contar_pagas → renomeado para Entrega.contar_atendidas (usa o scope atendidas).
  • Consolidacao#entregas_elegiveis_count passa a contar atendidas — reflete no wizard, na barra de progresso e no gate de finalização (todas_entregas_classificadas?): agora as entregas de insucesso também precisam ser classificadas antes de finalizar.
  • ConsolidacaoEntregasController: a lista do Passo 2 (validar) e o guard de elegibilidade (tracking_ids_elegiveis) usam Entrega.atendidas.
  • Não afeta o lado financeiro: dashboard, painel do motorista, job de 1h e HistoricoEstimado continuam usando .pagas (só completed) — "atendidas" é exclusivo da elegibilidade da consolidação.

📂 Arquivos alterados

app/models/entrega.rb                               # STATUS_ATENDIDO, scope :atendidas, contar_atendidas, falhada?
app/models/consolidacao.rb                          # entregas_elegiveis_count usa contar_atendidas
app/controllers/consolidacao_entregas_controller.rb # validar + tracking_ids_elegiveis usam :atendidas
app/views/consolidacao_entregas/validar.html.erb    # selo de status por entrega + destaque do insucesso

Sem migration — a mudança é apenas de leitura/regra de negócio sobre a tabela read-only db_reem_simplerout_2026.


🆕 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 classificadasmanual: true, igual aos apontamentos por NF).
  • A NF digitada aparece na Revisão (Passo 3), na tela da consolidação e no Relatório PDF do motorista (fallback nf_manual/obs_manual quando não há Entrega para consultar).

2. Arquivar motorista individualmente (reversível)

Numa consolidação em massa, dá para arquivar um motorista específico no Passo 1 (botão 🗄️ por card) — ele sai dos totais, do pagamento e do gate, mas os dados ficam guardados. Seção "🗄️ Motoristas arquivados" no fim do wizard com "♻️ Restaurar".

  • Não arquiva motorista já pago (estorne antes) — bloqueado com alerta.
  • Útil para finalizar a consolidação ignorando um motorista que não deveria estar nela / sem entregas, sem perder o registro.

⚙️ Como funciona (técnico)

  • Migrations (rodar db:migrate):
    • 20260625000001_add_arquivamento_to_consolidacao_motoristasarquivado_em / arquivado_por.
    • 20260625000002_add_manual_fields_to_consolidacao_entregasnf_manual / obs_manual.
  • Consolidacao#adicionar_apontamento_manual!(motorista:, tipos:, user:, nf:, obs:, valor_extra:) — gera tracking_id sintético ("MAN-<uuid>") compartilhado pelos pilares (agrupa como UMA entrega); reusa recalcular_motorista!. Action apontar_manual + rota + Stimulus apontamento_manual_controller.js.
  • ConsolidacaoMotorista: scopes ativos/arquivados, arquivar!/desarquivar!, arquivado?. Todo o "conjunto vigente" passou a usar .ativos (totais, pagamento, gate, notificações, PDFs financeiros, painel do motorista e filtros de pagamento do dashboard) — o arquivado não entra em nenhum agregado. Actions arquivar_motorista/desarquivar_motorista (autorização update?; bloqueiam motorista já pago).

📂 Principais arquivos

db/migrate/20260625000001_add_arquivamento_to_consolidacao_motoristas.rb
db/migrate/20260625000002_add_manual_fields_to_consolidacao_entregas.rb
app/models/consolidacao.rb · consolidacao_motorista.rb
app/controllers/consolidacoes_controller.rb · consolidacao_entregas_controller.rb
app/controllers/dashboard_controller.rb · motorista/dashboard_controller.rb
app/javascript/controllers/apontamento_manual_controller.js   (novo)
app/views/consolidacao_entregas/validar.html.erb · revisar.html.erb
app/views/consolidacoes/wizard.html.erb · show.html.erb
app/services/notificacao_service.rb · pdf/relatorio_motorista_pdf.rb · pdf/relatorio_financeiro_consolidacao_pdf.rb
config/routes.rb
spec/models/consolidacao_spec.rb · consolidacao_motorista_spec.rb
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íveis e fica verde com ✓ quando o carro está 100% classificado — atualiza em tempo real (sem recarregar) ao classificar.
  • A finalização continua por motorista (quando todos os carros de todos os motoristas estiverem prontos) — o gate não mudou.

⚙️ Como funciona (técnico)

  • ConsolidacaoEntregasController#veiculos_status(motorista)[{ vehicle, eligible, classificadas, fechado }] (2 queries: elegíveis com a coluna do veículo + classificadas não-manuais). Reusa o scope Entrega.da_veiculo. Grupo "sem veículo" via sentinela SEM_VEICULO.
  • validar aceita params[:vehicle]: recorta @entregas e, quando há carro selecionado, os totais da barra (@total_entregas/@classificadas) vêm do veiculos_status daquele carro.
  • tracking_ids_elegiveis(motorista, vehicle = nil) e classificar_em_massa passam o veículo — é o que faz "Marcar todos" fechar só o carro. resumo_json(motorista, vehicle:) devolve classificadas_escopo (barra por carro) + veiculos_status (refresh dos chips).
  • validacao_controller.js: value veiculo, target chipVeiculo, vehicle nos POSTs, atualizarChips() (verde/✓ + contadores). Sem migration — só leitura + UI.

📂 Arquivos

app/controllers/consolidacao_entregas_controller.rb  # veiculos_status, validar, tracking_ids_elegiveis, classificar_em_massa, resumo_json
app/views/consolidacao_entregas/validar.html.erb     # chips por veículo + barra com escopo
app/javascript/controllers/validacao_controller.js   # value veiculo, chips, classificadas_escopo

🆕 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, via CHAVES_MOEDA).
  • Lançado como uma linha por lote com a coluna nova quantidade; valor_aplicado guarda o total do lote (quantidade × preço), então recalcular_motorista! e todas as somas existentes seguem corretas. Modal com stepper N + (sem as setinhas nativas do input number).
  • Aparece na lista de Apontamentos manuais (badge "Entrega de Termo ×N"), nos PDFs (extrato, relatório do motorista e financeiro) e no painel de Resumo — sempre contando por quantidade (relatórios passaram de count para sum(:quantidade)).

2. 🧰 Caixa de ferramentas (toolbox) — botões unificados

  • Passo 2 (Validar): os botões "Registrar entrega manualmente" e "Entrega de termo" viraram um único " Adicionar lançamento ▾" com menu suspenso. (O antigo "Apontamento 100% manual" foi renomeado para "Registrar entrega manualmente" / modal "Registro manual de entrega".)
  • Passo 1 (Wizard): o "Apontamento manual" virou " Adicionar / Ferramentas ▾" com Apontamento manual + Adicionar motorista.
  • Reusa um controller Stimulus mínimo ferramentas_controller.js (só o dropdown); os modais e controllers existentes não foram reescritos.

3. 👷 Gestão de motoristas na consolidação

  • Adicionar motorista a uma consolidação já criada (ou restaurar se estava arquivado). O campo puxa os motoristas previstos no período (atendidas, respeitando filtros de veículo/ operação) com os veículos que cada um usou — lista clicável + datalist.
  • Excluir motorista arquivado (definitivo, só admin/gerente) — apaga os lançamentos dele (ligados por motorista_nome, sem cascade) e recalcula o total. Bloqueia se já pago. Arquivar continua sendo o caminho reversível.
  • O "← Voltar" do wizard passou a ir para o resumo da própria consolidação (não mais a lista).

4. 📊 Resumo e barra de progresso com lançamentos manuais

  • Painel "📊 Resumo" (Passo 2) ganhou linha Termo e linha "Apontamentos manuais: N".
  • Barra de progresso com 2 segmentos: laranja→verde (elegíveis classificadas) + azul (lançamentos manuais), com texto "· +N manuais". O gate de finalização não mudou (classificadas_count segue manual: false) — é feedback visual e entra no valor.
  • "Manuais" conta como entregas: cada termo vale sua quantidade; cada apontamento manual vale 1 por nota (tracking_id).

5. 🧊 UX da tela de Validar

  • Cabeçalho congelado (no desktop): header, barra de progresso, filtro por veículo, ações em massa e o botão de lançamento ficam fixos; só a lista de NFs rola (layout flex-column com a coluna da lista overflow-y-auto). Evitou-se sticky/z-index para não prender os modais (fixed z-50) atrás da sidebar (z-40).
  • Ações em massa simplificadas: removidos o botão gigante "Marcar todos como Normal" e o checkbox "Modo remover". Os botões de pilar em "Aplicar nos selecionados" agora são toggle em massa — aplicam o pilar nos selecionados e, se todos já o têm, removem de todos.

⚙️ Como funciona (técnico)

  • Migrations (rodar db:migrate):
    • 20260626000001_add_quantidade_to_consolidacao_entregasquantidade (default 1).
    • 20260626000002_add_preco_termo_configuracao — cria a config preco_termo (idempotente).
  • Consolidacao#adicionar_termos!(motorista:, quantidade:, user:) — uma linha TERMO-<uuid>, manual: true. Configuracao.preco_termo + ConsolidacaoEntrega.valor_para('termo').
  • ConsolidacoesController#adicionar_motorista / #excluir_motorista (member) + motoristas_previstos_no_periodo. ConsolidacaoEntregasController#apontar_termo, @manuais, contar_manuais, por_tipo agora sum(:quantidade).
  • Stimulus novos: apontamento_termo_controller.js, ferramentas_controller.js, adicionar_motorista_controller.js. validacao_controller.js: barra com 2 segmentos, linhas Termo/Manuais e toggle em massa (pilarAtivo); removidos marcarTodos/modoRemover.

📂 Principais arquivos

db/migrate/20260626000001_add_quantidade_to_consolidacao_entregas.rb
db/migrate/20260626000002_add_preco_termo_configuracao.rb
app/models/configuracao.rb · consolidacao.rb · consolidacao_entrega.rb
app/controllers/consolidacoes_controller.rb · consolidacao_entregas_controller.rb
app/javascript/controllers/apontamento_termo_controller.js   (novo)
app/javascript/controllers/ferramentas_controller.js         (novo)
app/javascript/controllers/adicionar_motorista_controller.js (novo)
app/javascript/controllers/validacao_controller.js
app/views/consolidacoes/wizard.html.erb · show.html.erb
app/views/consolidacao_entregas/validar.html.erb · revisar.html.erb
app/services/pdf/extrato_pdf.rb · relatorio_motorista_pdf.rb · relatorio_financeiro_consolidacao_pdf.rb
config/routes.rb · db/seeds.rb
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) usava flex gap-1.5 sem flex-wrap, então não quebrava linha e o último botão estourava a borda do card. Agora os botões quebram para a linha de baixo quando não cabem.
  • Botões sobrepondo o texto da nota no tablet (print 17, ~800px) — o card virava layout em linha já no breakpoint md (768px), exatamente onde a sidebar de 256px passa a aparecer (md:ml-64), espremendo o conteúdo. Sem flex-wrap, os botões transbordavam por cima do endereço/data/veículo. O ponto de virada foi movido para lg (1024px), onde há largura real.

⚙️ Como funciona (técnico)

  • app/views/consolidacao_entregas/validar.html.erb:
    • Card da entrega: flex flex-col md:flex-row md:items-centerflex 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.5flex flex-wrap gap-1.5 lg:shrink-0 (flex-wrap impede o transbordo; lg:shrink-0 mantém os botões inteiros no desktop, deixando o texto da nota truncar em vez de espremer os botões).
  • Faixa crítica resolvida (7681023px): antes acumulava sidebar visível + layout em linha + botões sem quebra. Agora < 1024px o card fica empilhado com botões em flex-wrap e ≥ 1024px vai para linha com truncamento do texto.
  • Sem migration e sem mudança de JS/controller — alteração somente de classes Tailwind na view.
🆕 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.
  • Painéis (espelham as imagens 2 e 3): KPIs (Total / Sucesso / Recusas / Pendentes), donut Insucessos %, Índices de Falha (por observation), Por Status (status_gade RECORRENTE/NOVO), NFs por Motorista, Entregas por STS/Unidade (contact_name), Entregas por Dia (barras empilhadas) e Mapa.
  • Cross-filter ao vivo (toggle) — clicar em qualquer linha das tabelas (Motorista, Unidade, Status, Observação) refiltra todo o dashboard por aquele valor; clicar de novo na linha ativa remove o filtro. Chips removíveis + "Limpar tudo" no topo.
  • Mapa de entregas com:
    • alternância 🔥 Calor (heatmap) ⇄ 📍 Pontos (um balão por entrega na coordenada de check-out);
    • camadas 🌙 Escuro (padrão) / 🗺️ Claro / 🛰️ Satélite (todas grátis, sem chave);
    • tela cheia (Fullscreen API);
    • popup por entrega (NF, destinatário, endereço, unidade, motorista, data) com a foto da fachada (coluna foto_da_fachada), aberta em tamanho cheio ao clicar.
  • Cores dos gráficos na identidade da marca: laranja #f97316 (completas) e vermelho #ef4444 (falhas); no comparativo A = laranja, B = azul.

⚙️ Como funciona (técnico)

  • Núcleo de dados em app/services/analytics/operacao_metricas.rb (PORO). Monta um SQL que espelha a query de gestão: CTE ROW_NUMBER() OVER (PARTITION BY reference_id ORDER BY checkout DESC) sobre db_reem_simplerout_2026 + INNER JOIN na tabela gade_entregas_* por reference_id::text = nota_fiscal; agrega tudo em Ruby (visão linhas = registros após o cross-filter).
  • Sem filtro de conta (account_id): o INNER JOIN com a tabela da operação já restringe ao cliente — replicar da_conta_gade zerava tudo quando DB_EXISTING_ACCOUNT_ID não batia.
  • Schema gade não-uniforme: só nota_fiscal é garantida. Colunas opcionais (status, nome_completo, endereco_completo) são detectadas por tabela (conn.columns) e viram NULL quando não existem — assim o UNION ALL entre operações no Global não quebra (era o erro column g.status does not exist).
  • Segurança: nomes de tabela passam por Operacao.sanitizar + quote_table_name; a foto_da_fachada é validada como URL http(s) antes de ir ao <img>, com escape de HTML no popup.
  • Performance do mapa: marcadores criados sob demanda (só ao abrir "Pontos") e o popup é uma função — a foto do S3 só é requisitada ao clicar no balão (nada é baixado no load). Limite de 2000 marcadores.
  • Filtro de período opcional no serviço (inicio:/fim:): aplicado só no Global.

📂 Arquivos

app/services/analytics/operacao_metricas.rb            # NOVO — núcleo de dados/agregações
app/controllers/operacoes_dashboard_controller.rb      # NOVO — modos + cross-filter
app/views/operacoes_dashboard/index.html.erb           # NOVO — filtros, abas, charts, mapa (JS)
app/views/operacoes_dashboard/_painel.html.erb         # NOVO — KPIs/donut/tabelas/gráfico
app/views/operacoes_dashboard/_comparativo.html.erb    # NOVO — tabela A|B|Δ + barras agrupadas
app/views/operacoes_dashboard/_mapa.html.erb           # NOVO — calor/pontos, camadas, tela cheia
app/views/operacoes_dashboard/_tabela_simples.html.erb # NOVO — tabela reutilizável + cross-filter
spec/services/analytics/operacao_metricas_spec.rb      # NOVO — specs das agregações/cross-filter
spec/requests/operacoes_dashboard_spec.rb              # NOVO — autorização por role + modos
config/routes.rb                                       # rota get /dashboard/operacoes
app/policies/dashboard_policy.rb                       # operacoes? (todos menos motorista)
app/views/layouts/_navbar.html.erb                     # link "📈 Operações"

Sem migration — tudo lê tabelas existentes (db_reem_simplerout_2026 read-only e gade_entregas_*). Libs front via CDN (Chart.js, Leaflet + leaflet.heat, flatpickr). ⚠️ Em produção, reiniciar o servidor (Puma) após o deploy — o Rails cacheia classes/views.

🆕 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), via Entrega::STATUS_ATENDIDO. Pino laranja = sucesso, azul = insucesso; o popup mostra o status e o motivo (observation). A foto da fachada é mantida para todos os casos.
  • Campo veículo — coluna vehicle (de Entrega) adicionada à query, ao card e à busca.
  • Modo escuro mais detalhado e com mais contraste — o tile escuro passou de CARTO dark_all (minimalista) para OpenStreetMap completo invertido (filter: invert(1) hue-rotate(180deg) brightness(.95) contrast(1.05) na classe .tiles-escuro-contraste): mesmo detalhe de ruas/nomes do mapa claro, porém escuro e legível. Claro/Satélite ficam intactos.

📊 Dashboard de Operações (painéis)

  • Gráficos clicáveis (cross-filter) — além das tabelas, agora o donut "Insucessos %" (clique em Completas/Falhas → filtra por status) e as barras "Entregas por Dia" (clique → filtra por dia + resultado da barra) refiltram todo o dashboard. Novos filtros f_resultado (coluna status) e f_data (por data de checkout, tratado à parte no serviço via @data). Chips removíveis + "Limpar tudo", igual às tabelas.
  • Falha em azul — a cor de insucesso mudou de vermelho #ef4444 para azul #3b82f6 nos KPIs e gráficos (mais contraste no tema escuro).
  • Hover nos cards de KPI — ao passar o mouse o card destaca, o número aumenta e aparece o % do total (no card Total, o detalhamento /●/).
  • Respiro no scroll das tabelaspr-2 nos contêineres roláveis (Motoristas / STS) para a barra de scroll não colar no texto.

🚚 Validação do motorista (consolidação)

  • Filtro de veículos multi-seleção — antes "um carro de cada vez"; agora dá pra somar 2+ veículos. A lista, a barra de progresso, o valor total (R$) e a contagem por pilar passam a refletir só os veículos selecionados. Chip selecionado fica laranja (prioridade sobre o verde de "fechado") para a seleção ficar sempre visível.
  • Lupa de busca na listagem de notas — campo 🔍 sticky no topo da lista que filtra os cards por NF, local, endereço ou veículo; "Selecionar todos" passa a marcar apenas os visíveis no filtro.
  • Barra fixa mais compacta — paddings/margens reduzidos (p-3 mb-3) para melhor aproveitamento em monitores pequenos.
  • Ocultar/mostrar o resumo lateral — botão 📊 que esconde o resumo e dá largura total à lista (lg:col-span-3 → lg:col-span-4).

🧭 Layout global (site inteiro)

  • Recolher/expandir a sidebar do menu principal — botão flutuante de seta (desktop) que esconde o menu e expande o conteúdo (md:ml-64 → 0). Estado lembrado entre páginas (localStorage + classe .sidebar-collapsed no <html>, aplicada antes de pintar → sem "piscar").
  • Indicador discreto — em repouso o botão é uma alça fina laranja sempre visível na borda; ao chegar perto com o mouse (ou foco por teclado) ela expande no botão com a seta ◀/▶.

📂 Arquivos

app/services/analytics/operacao_metricas.rb            # veículo, sucesso+insucesso, filtro por dia (@data)
app/controllers/operacoes_dashboard_controller.rb      # CROSS f_resultado + filtro f_data
app/views/operacoes_dashboard/index.html.erb           # busca/dark map, pinos, gráficos clicáveis, KPIs
app/views/operacoes_dashboard/_painel.html.erb         # hover KPIs, cor azul, pr-2 no scroll
app/views/operacoes_dashboard/_mapa.html.erb           # campo de busca no mapa
app/controllers/consolidacao_entregas_controller.rb    # filtro multi-veículo + resumo por escopo
app/views/consolidacao_entregas/validar.html.erb       # chips multi, lupa, barra compacta, toggle resumo
app/javascript/controllers/validacao_controller.js     # veiculos[] , busca, toggleResumo
app/views/layouts/application.html.erb                 # CSS sidebar recolhível + script anti-flash
app/views/layouts/_navbar.html.erb                     # botão flutuante + alça indicadora

Sem migration — segue lendo as tabelas existentes (db_reem_simplerout_2026 read-only e gade_entregas_*). ⚠️ Em produção, reiniciar o Puma após o deploy (cache de classes/views).

Description
No description provided
Readme 27 MiB
Languages
HTML 87%
Ruby 11.5%
JavaScript 1.3%
Shell 0.2%