Correção de alguns valores no dash principal

This commit is contained in:
2026-08-24 12:07:56 -03:00
parent 7b3ec3e407
commit 04b78bc7de
12 changed files with 690 additions and 50 deletions

View File

@@ -2,12 +2,20 @@
#
# Núcleo de dados do "Dashboard de Operações".
#
# Espelha a query de gestão do cliente: pega o ÚLTIMO status de cada NF em
# db_reem_simplerout_2026 (ROW_NUMBER por reference_id, checkout desc) e faz
# INNER JOIN com a(s) tabela(s) de operação (gade_entregas_*) por
# reference_id::text = nota_fiscal. Depois agrega tudo em Ruby para alimentar os
# painéis (KPIs, insucessos %, índices de falha, status, motoristas, STS, por dia
# e o mapa de calor).
# Espelha a query de gestão do cliente: casa db_reem_simplerout_2026 com a(s)
# tabela(s) de operação (gade_entregas_*) por reference_id::text = nota_fiscal e
# agrega em Ruby para alimentar os painéis (KPIs, insucessos %, índices de falha,
# status, motoristas, STS, por dia e o mapa de calor).
#
# DUAS CAMADAS, de propósito:
# #visitas — uma linha por ida do motorista ao local. É o que o dashboard
# financeiro conta (Entrega.contar_atendidas) e o que se paga ao
# motorista.
# #linhas — uma linha por NOTA FISCAL (a última visita de cada uma). É o que
# o cliente paga, o que confere com os documentos físicos e com a
# aba ENTREGAS da planilha. TODOS os KPIs desta tela saem daqui.
# Os dois números só coincidem quando nenhuma NF precisou de segunda ida; a
# diferença aparece na tela como "retentativas" em vez de ficar escondida.
#
# SEGURANÇA: os nomes das tabelas de operação só entram no SQL depois de passar
# pela whitelist (Operacao.sanitizar) + connection.quote_table_name — mesmo padrão
@@ -36,15 +44,30 @@ module Analytics
@tabelas.map { |t| Operacao.label(t) }.join(', ')
end
# Linhas após o cross-filter — base de TODAS as agregações/KPIs.
def linhas
@linhas ||= begin
# VISITAS após o cross-filter: uma linha por passagem do motorista. Uma mesma
# NF pode ter várias (falhou dia 10, entregou dia 12). É a base da análise
# operacional — quantas idas ao local foram necessárias.
def visitas
@visitas ||= begin
base = @filtros.empty? ? registros
: registros.select { |r| @filtros.all? { |col, val| r[col].to_s == val.to_s } }
@data ? base.select { |r| data_de(r['checkout']) == @data } : base
end
end
# NOTAS FISCAIS: uma linha por NF, com a ÚLTIMA visita dela. É a base de
# TODOS os KPIs desta tela, porque é o que confere com os documentos físicos
# e com a aba ENTREGAS da planilha entregue ao cliente (Analytics::
# PlanilhaEntregas também usa "último status por reference_id").
#
# ⚠️ O dedup precisa acontecer AQUI — depois do período e do cross-filter — e
# não no SQL. Enquanto ele rodava como ROW_NUMBER + `rn = 1` sobre a tabela
# INTEIRA, uma NF reentregue DEPOIS do fim do período perdia a visita que
# estava DENTRO dele e sumia da contagem do mês.
def linhas
@linhas ||= por_nota.values.map { |vs| vs.max_by { |r| ordem_visita(r) } }
end
# Campos pesquisáveis da tabela espelho (planilha da operação).
BUSCA_CAMPOS = %w[reference_id driver vehicle status observation contact_name
address nome_completo endereco_completo status_gade operacao].freeze
@@ -103,6 +126,34 @@ module Analytics
total - sucesso - recusas
end
# ── Camada operacional (por VISITA, não por NF) ───────────────
# Os KPIs acima contam NOTAS porque é o que o cliente paga e confere. Estes
# contam IDAS AO LOCAL — é o que o motorista recebe e o que o dashboard
# financeiro usa (Entrega.contar_atendidas conta linhas). Sem eles, uma NF
# que falhou duas vezes antes de ser entregue aparecia como 100% de sucesso
# e o insucesso sumia da tela.
def total_visitas
visitas.size
end
# Idas ao local além da primeira de cada NF.
def retentativas
total_visitas - total
end
def visitas_insucesso
visitas.count { |r| Entrega::STATUS_FALHA.include?(r['status']) }
end
# NFs que hoje constam como ENTREGUES mas custaram mais de uma ida — a
# informação que o dedup escondia por completo.
def notas_reentregues
@notas_reentregues ||= por_nota.count do |_nf, vs|
vs.any? { |r| Entrega::STATUS_FALHA.include?(r['status']) } &&
vs.max_by { |r| ordem_visita(r) }['status'] == 'completed'
end
end
# Donut "Insucessos %": completas vs falhas (sobre o total).
def insucessos_pct
{
@@ -206,6 +257,27 @@ module Analytics
private
# NF => visitas dela (já filtradas). Chave string: reference_id é numérico no
# espelho e texto na tabela da operação.
def por_nota
@por_nota ||= visitas.group_by { |r| r['reference_id'].to_s }
end
# Ordem de "última visita": quem tem checkout ganha de quem não tem e, entre
# as com checkout, vence a mais recente. Mesma regra do ROW_NUMBER que a
# planilha do cliente usa (checkout DESC NULLS LAST).
def ordem_visita(registro)
t = tempo_de(registro['checkout'])
[t ? 1 : 0, t || Time.at(0)]
end
def tempo_de(valor)
return nil if valor.nil? || valor.to_s.strip.empty?
valor.to_time
rescue ArgumentError, TypeError, NoMethodError
nil
end
def datas_checkout
@datas_checkout ||= linhas.filter_map { |r| data_de(r['checkout']) }
end
@@ -247,35 +319,33 @@ module Analytics
conn = ActiveRecord::Base.connection
rastreio = conn.quote_table_name(Entrega.table_name)
conta = Entrega.condicao_conta_sql('r')
unions = @tabelas.map do |tabela|
gade = conn.quote_table_name(tabela)
label = conn.quote(Operacao.label(tabela))
<<~SQL.strip
SELECT #{label} AS operacao,
r.tracking_id,
r.reference_id, r.driver, r.vehicle, r.status, r.observation, r.contact_name, r.address,
r.checkout, r.planned_date, r.foto_da_fachada,
r.latitude, r.longitude,
r.checkout_latitude, r.checkout_longitude,
#{selects_gade(conn, tabela)}
FROM ultimo r
FROM #{rastreio} r
INNER JOIN #{gade} g ON r.reference_id::text = g.nota_fiscal
WHERE r.rn = 1#{filtro_periodo(conn)}
WHERE r.reference_id IS NOT NULL#{conta ? " AND #{conta}" : ''}#{filtro_periodo(conn)}
SQL
end
# Sem filtro de conta: o INNER JOIN com a tabela da operação (gade_entregas_*)
# já restringe aos dados do cliente. Mantém a query idêntica à de gestão.
sql = <<~SQL
WITH ultimo AS (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY reference_id ORDER BY checkout DESC NULLS LAST) AS rn
FROM #{conn.quote_table_name(Entrega.table_name)}
WHERE reference_id IS NOT NULL
)
#{unions.join("\nUNION ALL\n")}
SQL
conn.select_all(sql).to_a
# Traz TODAS as visitas; a redução para uma linha por NF é feita em #linhas,
# já depois do período e do cross-filter (ver o comentário lá).
#
# `uniq` por tracking_id: se a mesma nota_fiscal estiver repetida dentro de
# uma tabela de operação (ou em duas tabelas do UNION), o INNER JOIN
# devolveria a MESMA visita mais de uma vez e inflaria a contagem.
conn.select_all(unions.join("\nUNION ALL\n")).to_a.uniq { |r| r['tracking_id'] }
end
# Colunas extras da tabela gade que só existem em ALGUMAS operações (ex.: