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' %> + +
+ <% @operacoes.each do |op| %> + + <% end %> +
+
+ <% end %> @@ -80,6 +99,30 @@ + <%# KPIs FINANCEIROS (consolidações finalizadas, já com filtro de operação) %> +
+
+

💰 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

+
+
+ <%# GRÁFICO + MOTORISTAS (lado a lado no desktop) %>
@@ -87,8 +130,8 @@
-

Evolução Diária

-

Valor estimado por dia no período

+

Evolução do custo

+

Valor estimado por dia no período<%= ' · operação filtrada' if @operacao_filtro.present? %>

Valor (R$) @@ -138,6 +181,43 @@
+ <%# ── FINANCEIRO POR OPERAÇÃO ───────────────────────────── %> +
+ + <%# Custo por operação (barra) %> +
+

🏷️ Custo por operação

+

Valor consolidado no período

+ <% if @fin_por_operacao.any? %> +
+ <% else %> +
🏷️

Sem dados no período

+ <% end %> +
+ + <%# Composição por tipo (rosca) %> +
+

🧩 Composição do custo

+

Por tipo de lançamento

+ <% if @fin_por_tipo.any? %> +
+ <% else %> +
🧩

Sem dados no período

+ <% end %> +
+ + <%# Custo por motorista (barra) %> +
+

👤 Custo por motorista

+

Top 10 no período

+ <% if @fin_por_motorista.any? %> +
+ <% else %> +
👤

Sem dados no período

+ <% end %> +
+
+ <%# ── PAGAMENTOS (pago x pendente) ──────────────────────── %>
@@ -299,6 +379,17 @@ })(); +<%# Filtro de operação — submete o form ao marcar/desmarcar %> + + <%# Chart.js via CDN %> @@ -422,3 +513,70 @@ }); })(); + +<%# ── Gráficos financeiros por operação ─────────────────────── %> +