Correção do dash board de acompanhamento diario das entregas
This commit is contained in:
@@ -17,13 +17,16 @@ class OperacoesDashboardController < ApplicationController
|
||||
|
||||
case @modo
|
||||
when 'global'
|
||||
@metricas = montar(@tabelas_validas)
|
||||
# Global: agrega TODAS as operações dentro da faixa de datas escolhida.
|
||||
@metricas = montar(@tabelas_validas, inicio: @periodo_inicio, fim: @periodo_fim)
|
||||
when 'comparar'
|
||||
# Comparar: cada operação inteira (sem filtro de data — só vale no Global).
|
||||
@op_a = Operacao.sanitizar([params[:op_a]]).first || @tabelas_validas[0]
|
||||
@op_b = Operacao.sanitizar([params[:op_b]]).first || @tabelas_validas[1]
|
||||
@metricas_a = montar([@op_a]) if @op_a
|
||||
@metricas_b = montar([@op_b]) if @op_b
|
||||
else
|
||||
# Operação única: a operação inteira (a data vem dos próprios dados).
|
||||
@operacao = Operacao.sanitizar([params[:operacao]]).first || @tabelas_validas.first
|
||||
@metricas = montar([@operacao]) if @operacao
|
||||
end
|
||||
@@ -31,8 +34,8 @@ class OperacoesDashboardController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def montar(tabelas)
|
||||
Analytics::OperacaoMetricas.new(tabelas: tabelas, inicio: @periodo_inicio, fim: @periodo_fim)
|
||||
def montar(tabelas, inicio: nil, fim: nil)
|
||||
Analytics::OperacaoMetricas.new(tabelas: tabelas, inicio: inicio, fim: fim)
|
||||
end
|
||||
|
||||
# Faixa de datas do filtro (params inicio/fim). Default: início do mês → hoje.
|
||||
|
||||
@@ -17,16 +17,29 @@ module Analytics
|
||||
# Centro padrão do mapa quando não há coordenadas (Grande São Paulo).
|
||||
SP_CENTRO = [-23.55, -46.63].freeze
|
||||
|
||||
def initialize(tabelas:, inicio:, fim:)
|
||||
# inicio/fim são OPCIONAIS: quando nil, não há filtro de data e o conjunto é a
|
||||
# própria operação inteira (visão de operação única). O filtro de período só é
|
||||
# usado na visão Global.
|
||||
def initialize(tabelas:, inicio: nil, fim: nil)
|
||||
@tabelas = Operacao.sanitizar(Array(tabelas))
|
||||
@inicio = inicio.to_date
|
||||
@fim = fim.to_date
|
||||
@inicio = inicio&.to_date
|
||||
@fim = fim&.to_date
|
||||
end
|
||||
|
||||
def operacoes_label
|
||||
@tabelas.map { |t| Operacao.label(t) }.join(', ')
|
||||
end
|
||||
|
||||
# Faixa real das entregas carregadas (pela data de checkout) — para exibir o
|
||||
# período natural da operação no cabeçalho/painel.
|
||||
def data_inicio
|
||||
datas_checkout.min
|
||||
end
|
||||
|
||||
def data_fim
|
||||
datas_checkout.max
|
||||
end
|
||||
|
||||
# Linhas cruas (Array de Hash com chave string) — base de todas as agregações.
|
||||
def registros
|
||||
@registros ||= carregar
|
||||
@@ -128,6 +141,10 @@ module Analytics
|
||||
|
||||
private
|
||||
|
||||
def datas_checkout
|
||||
@datas_checkout ||= registros.filter_map { |r| data_de(r['checkout']) }
|
||||
end
|
||||
|
||||
def contagem(rows, coluna)
|
||||
rows.group_by { |r| r[coluna].presence || '—' }
|
||||
.map { |nome, grupo| { nome: nome, total: grupo.size } }
|
||||
@@ -160,18 +177,19 @@ module Analytics
|
||||
conn = ActiveRecord::Base.connection
|
||||
|
||||
unions = @tabelas.map do |tabela|
|
||||
gade = conn.quote_table_name(tabela)
|
||||
label = conn.quote(Operacao.label(tabela))
|
||||
gade = conn.quote_table_name(tabela)
|
||||
label = conn.quote(Operacao.label(tabela))
|
||||
status = coluna_status_gade(conn, tabela)
|
||||
<<~SQL.strip
|
||||
SELECT #{label} AS operacao,
|
||||
r.driver, r.status, r.observation, r.contact_name,
|
||||
r.checkout, r.planned_date,
|
||||
r.latitude, r.longitude,
|
||||
r.checkout_latitude, r.checkout_longitude,
|
||||
g.status AS status_gade
|
||||
#{status} AS status_gade
|
||||
FROM ultimo r
|
||||
INNER JOIN #{gade} g ON r.reference_id::text = g.nota_fiscal
|
||||
WHERE r.rn = 1 AND (#{periodo_sql(conn)})
|
||||
WHERE r.rn = 1#{filtro_periodo(conn)}
|
||||
SQL
|
||||
end
|
||||
|
||||
@@ -188,16 +206,24 @@ module Analytics
|
||||
conn.select_all(sql).to_a
|
||||
end
|
||||
|
||||
# g.status existe só em ALGUMAS tabelas de operação (ex.: ubs_norte). Quando a
|
||||
# coluna não existe, devolve NULL — o painel "Por Status" cai no rótulo "—".
|
||||
# (A única coluna garantida em toda tabela gade é nota_fiscal.)
|
||||
def coluna_status_gade(conn, tabela)
|
||||
colunas = conn.columns(tabela).map(&:name)
|
||||
colunas.include?('status') ? 'g.status' : 'CAST(NULL AS text)'
|
||||
end
|
||||
|
||||
# Filtro de período OPCIONAL — usado só na visão Global (inicio/fim presentes).
|
||||
# Concluídas/falhas pela DATA REAL (checkout); pendentes (sem checkout) pela
|
||||
# data planejada. Espelha Entrega.no_periodo_checkout + no_periodo.
|
||||
def periodo_sql(conn)
|
||||
def filtro_periodo(conn)
|
||||
return '' unless @inicio && @fim
|
||||
ini = conn.quote(@inicio)
|
||||
fim_excl = conn.quote(@fim + 1)
|
||||
fim_dia = conn.quote(@fim.end_of_day)
|
||||
<<~SQL.strip
|
||||
(r.checkout >= #{ini} AND r.checkout < #{fim_excl})
|
||||
OR (r.checkout IS NULL AND r.planned_date >= #{ini} AND r.planned_date <= #{fim_dia})
|
||||
SQL
|
||||
" AND ((r.checkout >= #{ini} AND r.checkout < #{fim_excl})" \
|
||||
" OR (r.checkout IS NULL AND r.planned_date >= #{ini} AND r.planned_date <= #{fim_dia}))"
|
||||
end
|
||||
|
||||
# Replica Entrega.da_conta_gade como fragmento "AND (account_id ...)".
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
<% if titulo.present? %>
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-lg font-bold text-white"><%= titulo %></h2>
|
||||
<span class="text-xs text-gray-500"><%= metricas.total %> entregas</span>
|
||||
<span class="text-xs text-gray-500">
|
||||
<%= metricas.total %> entregas<% if metricas.data_inicio %> · <%= metricas.data_inicio.strftime('%d/%m/%Y') %>–<%= metricas.data_fim.strftime('%d/%m/%Y') %><% end %>
|
||||
</span>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
|
||||
@@ -46,30 +46,41 @@
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-white">Dashboard de Operações</h1>
|
||||
<p class="text-gray-400 text-sm mt-0.5">
|
||||
<%= @periodo_inicio.strftime('%d/%m/%Y') %> – <%= @periodo_fim.strftime('%d/%m/%Y') %>
|
||||
<% if @modo == 'global' %>
|
||||
🌐 Global · <%= @periodo_inicio.strftime('%d/%m/%Y') %> – <%= @periodo_fim.strftime('%d/%m/%Y') %>
|
||||
<% elsif @modo == 'comparar' %>
|
||||
⚖️ Comparação entre operações (cada uma no seu próprio período)
|
||||
<% elsif @metricas&.data_inicio %>
|
||||
<%= @metricas.operacoes_label %> · <%= @metricas.data_inicio.strftime('%d/%m/%Y') %> – <%= @metricas.data_fim.strftime('%d/%m/%Y') %>
|
||||
<% elsif @operacao %>
|
||||
<%= Operacao.label(@operacao) %>
|
||||
<% end %>
|
||||
· Atualizado às <%= Time.now.strftime('%H:%M') %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<%# Filtro de período (flatpickr) + atalhos %>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<div class="relative">
|
||||
<span class="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-orange-500">📅</span>
|
||||
<input type="text" id="periodo-range" readonly placeholder="Selecione o período"
|
||||
value="<%= @periodo_inicio.strftime('%d/%m/%Y') %> até <%= @periodo_fim.strftime('%d/%m/%Y') %>"
|
||||
class="cursor-pointer pl-4 pr-10 py-2.5 bg-[#1a1a1a] border border-white/10 rounded-xl
|
||||
text-white text-sm w-full sm:w-[240px] focus:outline-none focus:border-orange-500 placeholder-gray-500">
|
||||
<%# Filtro de período — só na visão Global (a faixa de data não se aplica a
|
||||
operação única, que usa o período natural dos próprios dados). %>
|
||||
<% if @modo == 'global' %>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<div class="relative">
|
||||
<span class="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-orange-500">📅</span>
|
||||
<input type="text" id="periodo-range" readonly placeholder="Selecione o período"
|
||||
value="<%= @periodo_inicio.strftime('%d/%m/%Y') %> até <%= @periodo_fim.strftime('%d/%m/%Y') %>"
|
||||
class="cursor-pointer pl-4 pr-10 py-2.5 bg-[#1a1a1a] border border-white/10 rounded-xl
|
||||
text-white text-sm w-full sm:w-[240px] focus:outline-none focus:border-orange-500 placeholder-gray-500">
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-1">
|
||||
<% atalhos.each do |label, (ini, fim)| %>
|
||||
<% ativo = @periodo_inicio == ini && @periodo_fim == fim %>
|
||||
<%= link_to label,
|
||||
operacoes_dashboard_path(modo: 'global', inicio: ini.strftime('%Y-%m-%d'), fim: fim.strftime('%Y-%m-%d')),
|
||||
data: { turbo: false },
|
||||
class: "px-3 py-2.5 rounded-xl text-sm whitespace-nowrap border #{ativo ? 'bg-orange-500 text-black border-orange-500 font-bold' : 'bg-[#1a1a1a] text-gray-300 border-white/10 hover:border-orange-500'}" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-1">
|
||||
<% atalhos.each do |label, (ini, fim)| %>
|
||||
<% ativo = @periodo_inicio == ini && @periodo_fim == fim %>
|
||||
<%= link_to label,
|
||||
operacoes_dashboard_path(ctx_ops.merge(modo: @modo, inicio: ini.strftime('%Y-%m-%d'), fim: fim.strftime('%Y-%m-%d'))),
|
||||
data: { turbo: false },
|
||||
class: "px-3 py-2.5 rounded-xl text-sm whitespace-nowrap border #{ativo ? 'bg-orange-500 text-black border-orange-500 font-bold' : 'bg-[#1a1a1a] text-gray-300 border-white/10 hover:border-orange-500'}" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%# ── Abas de modo ──────────────────────────────────────── %>
|
||||
|
||||
Reference in New Issue
Block a user