Implantação da função filtro de Operções exemplo UBS***

This commit is contained in:
2026-06-17 16:08:22 -03:00
parent e9def7e21a
commit c969049c1a
11 changed files with 202 additions and 7 deletions

View File

@@ -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)

View File

@@ -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 = <tabela_operacao>.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

70
app/models/operacao.rb Normal file
View File

@@ -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