diff --git a/Imagens para implantação /Imagem colada (11).png b/Imagens para implantação /Imagem colada (11).png
new file mode 100644
index 0000000..f68f767
Binary files /dev/null and b/Imagens para implantação /Imagem colada (11).png differ
diff --git a/app/controllers/consolidacao_entregas_controller.rb b/app/controllers/consolidacao_entregas_controller.rb
index 4cce105..e965b37 100644
--- a/app/controllers/consolidacao_entregas_controller.rb
+++ b/app/controllers/consolidacao_entregas_controller.rb
@@ -16,7 +16,8 @@ class ConsolidacaoEntregasController < ApplicationController
.da_conta_gade
.no_periodo(@consolidacao.data_inicio, @consolidacao.data_fim)
.do_motorista(@motorista)
- @entregas = @entregas.da_veiculo(@consolidacao.vehicle_ids) if @consolidacao.vehicle_ids.present?
+ @entregas = @entregas.da_veiculo(@consolidacao.vehicle_ids) if @consolidacao.vehicle_ids.present?
+ @entregas = @entregas.da_operacoes(@consolidacao.operacoes) if @consolidacao.operacoes.present?
@entregas = @entregas.order(:planned_date)
# Classificações já feitas — agrupadas por tracking_id (uma entrega pode ter
@@ -185,7 +186,8 @@ class ConsolidacaoEntregasController < ApplicationController
rel = Entrega.pagas.da_conta_gade
.no_periodo(@consolidacao.data_inicio, @consolidacao.data_fim)
.do_motorista(motorista)
- rel = rel.da_veiculo(@consolidacao.vehicle_ids) if @consolidacao.vehicle_ids.present?
+ rel = rel.da_veiculo(@consolidacao.vehicle_ids) if @consolidacao.vehicle_ids.present?
+ rel = rel.da_operacoes(@consolidacao.operacoes) if @consolidacao.operacoes.present?
rel.pluck(:tracking_id)
end
diff --git a/app/controllers/consolidacoes_controller.rb b/app/controllers/consolidacoes_controller.rb
index c3778c5..a78360d 100644
--- a/app/controllers/consolidacoes_controller.rb
+++ b/app/controllers/consolidacoes_controller.rb
@@ -46,12 +46,14 @@ class ConsolidacoesController < ApplicationController
)
@motoristas = Entrega.motoristas_ativos
@veiculos = Entrega.veiculos_unicos
+ @operacoes = Operacao.todas
end
# POST /consolidacoes
def create
authorize Consolidacao
@consolidacao = Consolidacao.new(consolidacao_params.merge(created_by: current_user.id))
+ @consolidacao.operacoes = Operacao.sanitizar(@consolidacao.operacoes)
motoristas = Array(params[:motoristas]).reject(&:blank?)
@@ -86,6 +88,19 @@ class ConsolidacoesController < ApplicationController
render json: { veiculos: veiculos }
end
+ # GET /consolidacoes/operacoes_disponiveis — lista (JSON) as tabelas de operação.
+ def operacoes_disponiveis
+ authorize Consolidacao, :new?
+ render json: { operacoes: Operacao.todas }
+ end
+
+ # GET /consolidacoes/dados_da_operacao?operacoes[]=X — motoristas/veículos/período
+ # derivados das operações selecionadas, para pré-preencher o formulário.
+ def dados_da_operacao
+ authorize Consolidacao, :new?
+ render json: Operacao.dados(params[:operacoes])
+ end
+
# GET /consolidacoes/:id — visão geral
def show
authorize @consolidacao
@@ -268,9 +283,10 @@ class ConsolidacoesController < ApplicationController
def preparar_form_new
@motoristas = Entrega.motoristas_ativos
@veiculos = Entrega.veiculos_unicos
+ @operacoes = Operacao.todas
end
def consolidacao_params
- params.require(:consolidacao).permit(:nome, :data_inicio, :data_fim, vehicle_ids: [])
+ params.require(:consolidacao).permit(:nome, :data_inicio, :data_fim, vehicle_ids: [], operacoes: [])
end
end
diff --git a/app/models/consolidacao.rb b/app/models/consolidacao.rb
index cdf350f..d893afc 100644
--- a/app/models/consolidacao.rb
+++ b/app/models/consolidacao.rb
@@ -130,10 +130,16 @@ class Consolidacao < ApplicationRecord
inicio: data_inicio,
fim: data_fim,
motorista: motorista,
- vehicle: vehicle_ids.presence
+ vehicle: vehicle_ids.presence,
+ operacoes: operacoes.presence
)
end
+ # Rótulos amigáveis das operações vinculadas (para exibição).
+ def operacoes_label
+ Array(operacoes).map { |t| Operacao.label(t) }
+ end
+
# Entrega conta como classificada se tiver ≥1 pilar (distinct por tracking_id,
# já que agora pode haver vários pilares por entrega).
def classificadas_count(motorista)
diff --git a/app/models/entrega.rb b/app/models/entrega.rb
index 3495f8b..0d28652 100644
--- a/app/models/entrega.rb
+++ b/app/models/entrega.rb
@@ -35,6 +35,21 @@ class Entrega < ApplicationRecord
where(vehicle: vehicle)
}
+ # Restringe às entregas cujas NFs (reference_id) constam na(s) tabela(s) de
+ # operação informada(s). O JOIN é o mesmo do script de gestão do cliente:
+ # reference_id::text = .nota_fiscal
+ #
+ # SEGURANÇA: o nome da tabela entra no SQL, então passa pela whitelist
+ # (Operacao.sanitizar) e por quote_table_name. Nomes inválidos são descartados;
+ # se nada sobrar, não filtra (all) — o filtro de operação é opcional.
+ scope :da_operacoes, ->(tabelas) {
+ validas = Operacao.sanitizar(tabelas)
+ next all if validas.empty?
+
+ selects = validas.map { |t| "SELECT nota_fiscal FROM #{connection.quote_table_name(t)}" }
+ where("reference_id::text IN (#{selects.join(' UNION ')})")
+ }
+
scope :do_mes, ->(data = Date.current) {
where(planned_date: data.beginning_of_month..data.end_of_month)
}
@@ -84,10 +99,11 @@ class Entrega < ApplicationRecord
end
# Contagem de entregas pagas para cálculo estimado
- def self.contar_pagas(inicio:, fim:, motorista: nil, vehicle: nil)
+ def self.contar_pagas(inicio:, fim:, motorista: nil, vehicle: nil, operacoes: nil)
base = pagas.da_conta_gade.no_periodo(inicio, fim)
- base = base.do_motorista(motorista) if motorista.present?
- base = base.da_veiculo(vehicle) if vehicle.present?
+ base = base.do_motorista(motorista) if motorista.present?
+ base = base.da_veiculo(vehicle) if vehicle.present?
+ base = base.da_operacoes(operacoes) if operacoes.present?
base.count
end
diff --git a/app/models/operacao.rb b/app/models/operacao.rb
new file mode 100644
index 0000000..d4b99a8
--- /dev/null
+++ b/app/models/operacao.rb
@@ -0,0 +1,70 @@
+# app/models/operacao.rb
+#
+# Operações são as tabelas EXTERNAS que o cliente carrega no banco, uma por
+# operação/mês (ex.: "gade_entregas_ubs_sul_leste_jun_2026"). Cada tabela tem a
+# coluna `nota_fiscal`, que casa com db_reem_simplerout_2026.reference_id::text.
+#
+# Estas tabelas são SOMENTE LEITURA — apenas SELECT, nunca migration/INSERT/etc.
+#
+# Como o nome da tabela entra em SQL (FROM dinâmico), TODA entrada externa passa
+# por uma whitelist (#nomes_validos) antes de ser usada — proteção anti-injection.
+class Operacao
+ PREFIXO = ENV.fetch('OPERACOES_TABLE_PREFIX', 'gade_entregas_').freeze
+
+ class << self
+ # [{ tabela: 'gade_entregas_...', label: 'Ubs Sul Leste Jun 2026' }, ...]
+ def todas
+ nomes_validos.map { |t| { tabela: t, label: label(t) } }
+ end
+
+ # Nomes reais de tabelas no schema public que batem com o prefixo.
+ # Sem cache de processo: o cliente sobe novas tabelas de operação a qualquer
+ # momento e elas precisam aparecer sem reiniciar o servidor (a consulta ao
+ # catálogo é barata).
+ def nomes_validos
+ conn.select_values(
+ "SELECT table_name FROM information_schema.tables " \
+ "WHERE table_schema = 'public' AND table_name LIKE #{conn.quote("#{PREFIXO}%")} ORDER BY table_name"
+ )
+ end
+
+ # Interseção da lista recebida com as tabelas que existem de fato.
+ def sanitizar(lista)
+ Array(lista).map(&:to_s).uniq & nomes_validos
+ end
+
+ def valida?(nome)
+ nomes_validos.include?(nome.to_s)
+ end
+
+ # Rótulo amigável: tira o prefixo e formata.
+ def label(tabela)
+ tabela.to_s.delete_prefix(PREFIXO).tr('_', ' ').strip.titleize
+ end
+
+ # Dados agregados das entregas das operações informadas, para pré-preencher o
+ # formulário de nova consolidação. Une as NFs de todas as tabelas válidas.
+ def dados(tabelas)
+ validas = sanitizar(tabelas)
+ return vazio if validas.empty?
+
+ entregas = Entrega.da_conta_gade.da_operacoes(validas)
+ {
+ motoristas: entregas.distinct.order(:driver).pluck(:driver).compact.reject(&:blank?),
+ veiculos: entregas.distinct.order(:vehicle).pluck(:vehicle).compact.reject(&:blank?),
+ inicio: entregas.minimum(:planned_date)&.to_date,
+ fim: entregas.maximum(:planned_date)&.to_date
+ }
+ end
+
+ private
+
+ def vazio
+ { motoristas: [], veiculos: [], inicio: nil, fim: nil }
+ end
+
+ def conn
+ ActiveRecord::Base.connection
+ end
+ end
+end
diff --git a/app/views/consolidacoes/index.html.erb b/app/views/consolidacoes/index.html.erb
index e052590..49b0270 100644
--- a/app/views/consolidacoes/index.html.erb
+++ b/app/views/consolidacoes/index.html.erb
@@ -55,6 +55,9 @@
· 👤 <%= c.consolidacao_motoristas.count %> motorista(s)
· Criada por <%= c.criador.nome_display %>
<% end %>
+ <%# Operação — multi-select opcional (pré-preenche e filtra por NF) %>
+
+
+
+ <% if @operacoes.any? %>
+ <% @operacoes.each do |op| %>
+
+ <% end %>
+ <% else %>
+
Nenhuma tabela de operação encontrada no banco.
+ <% end %>
+
+
+
+
<%# Nome %>
<%= f.label :nome, 'Nome da consolidação', class: 'block text-white font-semibold mb-2' %>
@@ -159,6 +181,49 @@
if (dataIni) dataIni.addEventListener('change', atualizarVeiculos);
if (dataFim) dataFim.addEventListener('change', atualizarVeiculos);
+ // ── Operações: ao marcar, pré-preenche período + motoristas + veículos ──
+ const opStatus = document.getElementById('operacao-status');
+ const dadosOpUrl = '<%= dados_da_operacao_consolidacoes_path %>';
+
+ function operacoesSelecionadas() {
+ return Array.from(document.querySelectorAll('.chk-operacao:checked')).map(c => c.value);
+ }
+
+ async function aplicarOperacoes() {
+ const ops = operacoesSelecionadas();
+ if (!ops.length) { if (opStatus) opStatus.textContent = ''; return; }
+
+ if (opStatus) opStatus.textContent = 'Carregando dados da operação…';
+ const params = new URLSearchParams();
+ ops.forEach(o => params.append('operacoes[]', o));
+
+ try {
+ const resp = await fetch(dadosOpUrl + '?' + params.toString(), { headers: { 'Accept': 'application/json' } });
+ const data = await resp.json();
+
+ if (data.inicio && dataIni) dataIni.value = data.inicio;
+ if (data.fim && dataFim) dataFim.value = data.fim;
+
+ const motset = new Set(data.motoristas || []);
+ document.querySelectorAll('.chk-motorista').forEach(c => { if (motset.has(c.value)) c.checked = true; });
+
+ await atualizarVeiculos();
+ const vehset = new Set(data.veiculos || []);
+ lista.querySelectorAll('input[name="consolidacao[vehicle_ids][]"]').forEach(c => { if (vehset.has(c.value)) c.checked = true; });
+
+ if (opStatus) {
+ opStatus.textContent = (data.motoristas || []).length + ' motorista(s) e ' +
+ (data.veiculos || []).length + ' veículo(s) nesta(s) operação(ões).';
+ }
+ } catch (e) {
+ if (opStatus) opStatus.textContent = 'Erro ao carregar dados da operação.';
+ }
+ }
+
+ document.addEventListener('change', function (e) {
+ if (e.target.classList.contains('chk-operacao')) aplicarOperacoes();
+ });
+
// Estado inicial (caso o form volte com motoristas já marcados após erro de validação)
if (motoristasSelecionados().length) atualizarVeiculos();
})();
diff --git a/app/views/consolidacoes/show.html.erb b/app/views/consolidacoes/show.html.erb
index 873c2e1..0a451d7 100644
--- a/app/views/consolidacoes/show.html.erb
+++ b/app/views/consolidacoes/show.html.erb
@@ -21,6 +21,14 @@
· Finalizada por <%= @consolidacao.finalizador&.nome_display %> em <%= l @consolidacao.finalizado_em, format: :short %>
<% end %>
+ <% if @consolidacao.operacoes.present? %>
+
+ 🗂️ Operação:
+ <% @consolidacao.operacoes_label.each do |op| %>
+ <%= op %>
+ <% end %>
+
+ <% end %>
<% if @consolidacao.rascunho? %>
diff --git a/config/routes.rb b/config/routes.rb
index 571ee62..b5ede08 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -27,6 +27,8 @@ Rails.application.routes.draw do
resources :consolidacoes do
collection do
get :veiculos_disponiveis # JSON — veículos do(s) motorista(s) no período (form nova consolidação)
+ get :operacoes_disponiveis # JSON — tabelas de operação (gade_entregas_*)
+ get :dados_da_operacao # JSON — motoristas/veículos/período da(s) operação(ões)
get :arquivadas # Histórico — consolidações arquivadas
end
diff --git a/db/migrate/20260617000002_add_operacoes_to_consolidacoes.rb b/db/migrate/20260617000002_add_operacoes_to_consolidacoes.rb
new file mode 100644
index 0000000..510ed5c
--- /dev/null
+++ b/db/migrate/20260617000002_add_operacoes_to_consolidacoes.rb
@@ -0,0 +1,7 @@
+# Operações (tabelas externas gade_entregas_*) vinculadas a uma consolidação.
+# Array de nomes de tabela já validados contra a whitelist (Operacao).
+class AddOperacoesToConsolidacoes < ActiveRecord::Migration[7.1]
+ def change
+ add_column :consolidacoes, :operacoes, :jsonb, default: [] unless column_exists?(:consolidacoes, :operacoes)
+ end
+end