Implantação da função filtro de Operções exemplo UBS***
This commit is contained in:
BIN
Imagens para implantação /Imagem colada (11).png
Normal file
BIN
Imagens para implantação /Imagem colada (11).png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 698 KiB |
@@ -17,6 +17,7 @@ class ConsolidacaoEntregasController < ApplicationController
|
|||||||
.no_periodo(@consolidacao.data_inicio, @consolidacao.data_fim)
|
.no_periodo(@consolidacao.data_inicio, @consolidacao.data_fim)
|
||||||
.do_motorista(@motorista)
|
.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)
|
@entregas = @entregas.order(:planned_date)
|
||||||
|
|
||||||
# Classificações já feitas — agrupadas por tracking_id (uma entrega pode ter
|
# Classificações já feitas — agrupadas por tracking_id (uma entrega pode ter
|
||||||
@@ -186,6 +187,7 @@ class ConsolidacaoEntregasController < ApplicationController
|
|||||||
.no_periodo(@consolidacao.data_inicio, @consolidacao.data_fim)
|
.no_periodo(@consolidacao.data_inicio, @consolidacao.data_fim)
|
||||||
.do_motorista(motorista)
|
.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)
|
rel.pluck(:tracking_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -46,12 +46,14 @@ class ConsolidacoesController < ApplicationController
|
|||||||
)
|
)
|
||||||
@motoristas = Entrega.motoristas_ativos
|
@motoristas = Entrega.motoristas_ativos
|
||||||
@veiculos = Entrega.veiculos_unicos
|
@veiculos = Entrega.veiculos_unicos
|
||||||
|
@operacoes = Operacao.todas
|
||||||
end
|
end
|
||||||
|
|
||||||
# POST /consolidacoes
|
# POST /consolidacoes
|
||||||
def create
|
def create
|
||||||
authorize Consolidacao
|
authorize Consolidacao
|
||||||
@consolidacao = Consolidacao.new(consolidacao_params.merge(created_by: current_user.id))
|
@consolidacao = Consolidacao.new(consolidacao_params.merge(created_by: current_user.id))
|
||||||
|
@consolidacao.operacoes = Operacao.sanitizar(@consolidacao.operacoes)
|
||||||
|
|
||||||
motoristas = Array(params[:motoristas]).reject(&:blank?)
|
motoristas = Array(params[:motoristas]).reject(&:blank?)
|
||||||
|
|
||||||
@@ -86,6 +88,19 @@ class ConsolidacoesController < ApplicationController
|
|||||||
render json: { veiculos: veiculos }
|
render json: { veiculos: veiculos }
|
||||||
end
|
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
|
# GET /consolidacoes/:id — visão geral
|
||||||
def show
|
def show
|
||||||
authorize @consolidacao
|
authorize @consolidacao
|
||||||
@@ -268,9 +283,10 @@ class ConsolidacoesController < ApplicationController
|
|||||||
def preparar_form_new
|
def preparar_form_new
|
||||||
@motoristas = Entrega.motoristas_ativos
|
@motoristas = Entrega.motoristas_ativos
|
||||||
@veiculos = Entrega.veiculos_unicos
|
@veiculos = Entrega.veiculos_unicos
|
||||||
|
@operacoes = Operacao.todas
|
||||||
end
|
end
|
||||||
|
|
||||||
def consolidacao_params
|
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
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -130,10 +130,16 @@ class Consolidacao < ApplicationRecord
|
|||||||
inicio: data_inicio,
|
inicio: data_inicio,
|
||||||
fim: data_fim,
|
fim: data_fim,
|
||||||
motorista: motorista,
|
motorista: motorista,
|
||||||
vehicle: vehicle_ids.presence
|
vehicle: vehicle_ids.presence,
|
||||||
|
operacoes: operacoes.presence
|
||||||
)
|
)
|
||||||
end
|
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,
|
# Entrega conta como classificada se tiver ≥1 pilar (distinct por tracking_id,
|
||||||
# já que agora pode haver vários pilares por entrega).
|
# já que agora pode haver vários pilares por entrega).
|
||||||
def classificadas_count(motorista)
|
def classificadas_count(motorista)
|
||||||
|
|||||||
@@ -35,6 +35,21 @@ class Entrega < ApplicationRecord
|
|||||||
where(vehicle: vehicle)
|
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) {
|
scope :do_mes, ->(data = Date.current) {
|
||||||
where(planned_date: data.beginning_of_month..data.end_of_month)
|
where(planned_date: data.beginning_of_month..data.end_of_month)
|
||||||
}
|
}
|
||||||
@@ -84,10 +99,11 @@ class Entrega < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Contagem de entregas pagas para cálculo estimado
|
# 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 = pagas.da_conta_gade.no_periodo(inicio, fim)
|
||||||
base = base.do_motorista(motorista) if motorista.present?
|
base = base.do_motorista(motorista) if motorista.present?
|
||||||
base = base.da_veiculo(vehicle) if vehicle.present?
|
base = base.da_veiculo(vehicle) if vehicle.present?
|
||||||
|
base = base.da_operacoes(operacoes) if operacoes.present?
|
||||||
base.count
|
base.count
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
70
app/models/operacao.rb
Normal file
70
app/models/operacao.rb
Normal 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
|
||||||
@@ -55,6 +55,9 @@
|
|||||||
· 👤 <%= c.consolidacao_motoristas.count %> motorista(s)
|
· 👤 <%= c.consolidacao_motoristas.count %> motorista(s)
|
||||||
· Criada por <%= c.criador.nome_display %>
|
· Criada por <%= c.criador.nome_display %>
|
||||||
</p>
|
</p>
|
||||||
|
<% if c.operacoes.present? %>
|
||||||
|
<p class="text-gray-500 text-xs mt-1">🗂️ <%= c.operacoes_label.join(' · ') %></p>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
<p class="text-orange-500 font-black text-2xl"><%= moeda(c.valor_total) %></p>
|
<p class="text-orange-500 font-black text-2xl"><%= moeda(c.valor_total) %></p>
|
||||||
|
|||||||
@@ -17,6 +17,28 @@
|
|||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
<%# Operação — multi-select opcional (pré-preenche e filtra por NF) %>
|
||||||
|
<div>
|
||||||
|
<label class="block text-white font-semibold mb-2">
|
||||||
|
🗂️ Operação <span class="text-gray-500 font-normal text-sm">(opcional — preenche motoristas, veículos e período, e filtra as entregas por nota fiscal)</span>
|
||||||
|
</label>
|
||||||
|
<div id="operacoes-lista"
|
||||||
|
class="bg-[#0a0a0a] border border-[#2a2a2a] rounded-lg p-3 max-h-48 overflow-y-auto space-y-1">
|
||||||
|
<% if @operacoes.any? %>
|
||||||
|
<% @operacoes.each do |op| %>
|
||||||
|
<label class="flex items-center gap-2 text-white hover:bg-[#1a1a1a] rounded px-2 py-2 cursor-pointer text-sm min-h-[44px]">
|
||||||
|
<%= check_box_tag 'consolidacao[operacoes][]', op[:tabela], false,
|
||||||
|
class: 'chk-operacao rounded text-orange-500 bg-[#1a1a1a] border-[#2a2a2a]', id: nil %>
|
||||||
|
<%= op[:label] %>
|
||||||
|
</label>
|
||||||
|
<% end %>
|
||||||
|
<% else %>
|
||||||
|
<p class="text-gray-500 text-sm px-2 py-2">Nenhuma tabela de operação encontrada no banco.</p>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<p id="operacao-status" class="text-gray-500 text-xs mt-1"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<%# Nome %>
|
<%# Nome %>
|
||||||
<div>
|
<div>
|
||||||
<%= f.label :nome, 'Nome da consolidação', class: 'block text-white font-semibold mb-2' %>
|
<%= 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 (dataIni) dataIni.addEventListener('change', atualizarVeiculos);
|
||||||
if (dataFim) dataFim.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)
|
// Estado inicial (caso o form volte com motoristas já marcados após erro de validação)
|
||||||
if (motoristasSelecionados().length) atualizarVeiculos();
|
if (motoristasSelecionados().length) atualizarVeiculos();
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -21,6 +21,14 @@
|
|||||||
· Finalizada por <%= @consolidacao.finalizador&.nome_display %> em <%= l @consolidacao.finalizado_em, format: :short %>
|
· Finalizada por <%= @consolidacao.finalizador&.nome_display %> em <%= l @consolidacao.finalizado_em, format: :short %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</p>
|
</p>
|
||||||
|
<% if @consolidacao.operacoes.present? %>
|
||||||
|
<p class="text-gray-400 text-sm mt-1">
|
||||||
|
🗂️ Operação:
|
||||||
|
<% @consolidacao.operacoes_label.each do |op| %>
|
||||||
|
<span class="inline-block bg-[#2a2a2a] text-gray-200 rounded-full px-2.5 py-0.5 text-xs"><%= op %></span>
|
||||||
|
<% end %>
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<% if @consolidacao.rascunho? %>
|
<% if @consolidacao.rascunho? %>
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ Rails.application.routes.draw do
|
|||||||
resources :consolidacoes do
|
resources :consolidacoes do
|
||||||
collection do
|
collection do
|
||||||
get :veiculos_disponiveis # JSON — veículos do(s) motorista(s) no período (form nova consolidação)
|
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
|
get :arquivadas # Histórico — consolidações arquivadas
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user