diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index f56ec92..d744460 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -33,8 +33,13 @@ class DashboardController < ApplicationController
end
def carregar_dados_dashboard
+ # Filtro de operação (multi-seleção) — aplicado a todo o dashboard.
+ @operacoes = Operacao.todas
+ @operacao_filtro = Operacao.sanitizar(params[:operacoes])
+
# Entregas do período (tabela read-only) — restritas à conta Gade
entregas = Entrega.da_conta_gade.no_periodo(@periodo_inicio, @periodo_fim)
+ entregas = entregas.da_operacoes(@operacao_filtro) if @operacao_filtro.present?
# Totais gerais
@total_entregas = entregas.count
@@ -76,6 +81,7 @@ class DashboardController < ApplicationController
# — independe de quando foram criadas (created_at).
@consolidacoes_mes = Consolidacao.ativas
.where('data_inicio <= ? AND data_fim >= ?', @periodo_fim, @periodo_inicio)
+ @consolidacoes_mes = @consolidacoes_mes.com_operacoes(@operacao_filtro) if @operacao_filtro.present?
@consolidacoes_abertas = @consolidacoes_mes.where(status: :rascunho).count
@consolidacoes_fechadas = @consolidacoes_mes.where(status: :finalizada).count
@@ -83,6 +89,9 @@ class DashboardController < ApplicationController
# (medido por motorista — fonte: consolidacao_motoristas).
carregar_dados_pagamentos
+ # Visão financeira por operação (KPIs + gráficos)
+ carregar_dados_financeiro
+
# Histórico estimado mais recente
@historico_recente = HistoricoEstimado.order(created_at: :desc).limit(5)
end
@@ -96,6 +105,9 @@ class DashboardController < ApplicationController
.joins(:consolidacao)
.where(consolidacoes: { status: :finalizada, deleted_at: nil })
.where(pago_em: intervalo)
+ if @operacao_filtro.present?
+ pagos = pagos.where(consolidacao_id: Consolidacao.com_operacoes(@operacao_filtro).select(:id))
+ end
@pag_pago_qtd = pagos.count
@pag_pago_valor = pagos.sum(:valor_total)
@@ -110,6 +122,48 @@ class DashboardController < ApplicationController
@pagamentos_pendentes = pendentes.includes(:consolidacao).order(consolidacao_id: :desc)
end
+ # Visão financeira (custo) das consolidações finalizadas do período, já com o
+ # filtro de operação aplicado.
+ def carregar_dados_financeiro
+ consols = Consolidacao.ativas.where(status: :finalizada)
+ .where('data_inicio <= ? AND data_fim >= ?', @periodo_fim, @periodo_inicio)
+ consols = consols.com_operacoes(@operacao_filtro) if @operacao_filtro.present?
+ consols = consols.to_a
+
+ ids = consols.map(&:id)
+ cms = ConsolidacaoMotorista.where(consolidacao_id: ids)
+ ces = ConsolidacaoEntrega.where(consolidacao_id: ids)
+
+ # KPIs
+ @fin_custo_total = cms.sum(:valor_total)
+ @fin_qtd_entregas = ces.distinct.count(:tracking_id)
+ @fin_ticket_medio = @fin_qtd_entregas.zero? ? 0 : (@fin_custo_total / @fin_qtd_entregas)
+ @fin_pago = cms.pagos.sum(:valor_total)
+ @fin_pendente = cms.pendentes.sum(:valor_total)
+
+ # Custo por operação — rateia o valor da consolidação igualmente entre suas
+ # operações (mantém o total consistente; exato no caso de 1 operação).
+ por_op = Hash.new(0)
+ consols.each do |c|
+ ops = c.operacoes.presence || ['(sem operação)']
+ fatia = c.valor_total / ops.size
+ ops.each { |o| por_op[o == '(sem operação)' ? o : Operacao.label(o)] += fatia }
+ end
+ @fin_por_operacao = por_op.sort_by { |_, v| -v }
+
+ # Composição por tipo (normal/retirada/bônus/desconto)
+ soma_tipo = ces.group(:tipo).sum(:valor_aplicado)
+ @fin_por_tipo = ConsolidacaoEntrega::TIPO_CORES.filter_map do |tipo, cfg|
+ valor = soma_tipo[tipo].to_f
+ next if valor.zero?
+ { label: cfg[:label], valor: valor }
+ end
+
+ # Custo por motorista (top 10)
+ @fin_por_motorista = cms.group(:motorista_nome).sum(:valor_total)
+ .sort_by { |_, v| -v }.first(10)
+ end
+
def build_grafico_diario(entregas, preco_entrega)
dias = (@periodo_inicio..@periodo_fim).to_a
diff --git a/app/models/consolidacao.rb b/app/models/consolidacao.rb
index d893afc..0f8a62e 100644
--- a/app/models/consolidacao.rb
+++ b/app/models/consolidacao.rb
@@ -48,6 +48,16 @@ class Consolidacao < ApplicationRecord
.select(:consolidacao_id))
}
+ # Consolidações que contêm QUALQUER das operações informadas (jsonb).
+ # SEGURANÇA: os nomes passam por whitelist (Operacao.sanitizar) e por quote.
+ scope :com_operacoes, ->(ops) {
+ validas = Operacao.sanitizar(ops)
+ next all if validas.empty?
+
+ lista = validas.map { |o| connection.quote(o) }.join(',')
+ where("EXISTS (SELECT 1 FROM jsonb_array_elements_text(COALESCE(operacoes, '[]'::jsonb)) e WHERE e IN (#{lista}))")
+ }
+
# ── Métodos ─────────────────────────────────────────────────
def arquivar!(user)
diff --git a/app/views/dashboard/index.html.erb b/app/views/dashboard/index.html.erb
index 4620042..d8ec9ef 100644
--- a/app/views/dashboard/index.html.erb
+++ b/app/views/dashboard/index.html.erb
@@ -30,6 +30,25 @@
+
+ <%# Filtro de operação — multi-seleção; submete o form ao mudar %>
+ <% if @operacoes.any? %>
+
+ 🗂️ <%= @operacao_filtro.present? ? "#{@operacao_filtro.size} operação(ões)" : 'Todas as operações' %>
+
+
💰 Custo total
+<%= moeda(@fin_custo_total) %>
+<%= @fin_qtd_entregas %> entregas classificadas
+🎫 Ticket médio
+<%= moeda(@fin_ticket_medio) %>
+por entrega
+✅ Pago
+<%= moeda(@fin_pago) %>
+já confirmado
+🕗 A pagar
+<%= moeda(@fin_pendente) %>
+pendente
+Valor estimado por dia no período
+Valor estimado por dia no período<%= ' · operação filtrada' if @operacao_filtro.present? %>
Valor consolidado no período
+ <% if @fin_por_operacao.any? %> + + <% else %> +Sem dados no período
Por tipo de lançamento
+ <% if @fin_por_tipo.any? %> + + <% else %> +Sem dados no período
Top 10 no período
+ <% if @fin_por_motorista.any? %> + + <% else %> +Sem dados no período