Files
Reem-Notas/app/services/analytics/totais_por_motorista.rb

148 lines
6.4 KiB
Ruby
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# app/services/analytics/totais_por_motorista.rb
#
# Quanto cada motorista somou no período — e QUAIS consolidações formam esse
# total. É a fonte única das duas telas de "total por motorista":
#
# • admin → /consolidacoes?visao=motoristas (modo de visualização da própria
# lista, com drill-down no motorista clicado)
# • motorista→ /motorista (o painel, já recortado nele — mesma conta, mesmo PDF)
#
# RECORTE — É O MESMO DO DASHBOARD FINANCEIRO, de propósito
# (DashboardController#carregar_dados_financeiro). Os dois números são lidos lado
# a lado pelo operador; quando divergem, a leitura é "o sistema está errado".
# Entram as consolidações ATIVAS que:
# • CRUZAM o período (data_inicio <= fim AND data_fim >= inicio) — um
# fechamento de 01/08 a 31/08 aparece ao filtrar 01/0826/08; OU
# • tiveram algum PAGAMENTO dentro do período (pagar em agosto um fechamento
# de julho tem que aparecer em agosto).
#
# STATUS: por padrão só as FINALIZADAS — é o "Consolidado" do dashboard, o valor
# que já fechou. 'todos' inclui os rascunhos em andamento (a visão "Geral"), que
# ainda mudam de valor.
#
# Só motoristas ATIVOS (não arquivados) contam: é o mesmo conjunto que a
# consolidação paga (ConsolidacaoMotorista.ativos), então o total desta tela
# bate com a soma dos fechamentos.
module Analytics
class TotaisPorMotorista
# status: 'finalizada' (padrão — só o que já fechou), 'rascunho' ou
# 'todos'. É o MESMO select de status da lista de consolidações.
# motorista: recorte exato (drill-down / painel do motorista).
# busca: filtro parcial por nome, o campo de busca da lista.
def initialize(inicio:, fim:, motorista: nil, status: 'finalizada', busca: nil)
@inicio = inicio.to_date
@fim = fim.to_date
@motorista = motorista.presence
@status = status.presence || 'finalizada'
@busca = busca.presence
end
attr_reader :inicio, :fim, :motorista, :status
# ConsolidacaoMotorista do recorte, com a consolidação já carregada (as duas
# telas mostram nome/período/status dela em seguida).
def registros
@registros ||= begin
base = Consolidacao.ativas
# `public_send` sobre o enum: quem traduz :finalizada/:rascunho é o model.
base = base.public_send(@status) if %w[finalizada rascunho].include?(@status)
escopo = ConsolidacaoMotorista.ativos
.where(consolidacao_id: ids_do_periodo(base))
.includes(:consolidacao)
escopo = escopo.where(motorista_nome: @motorista) if @motorista
escopo = escopo.where('motorista_nome ILIKE ?', "%#{@busca}%") if @busca
escopo.to_a
end
end
# Uma linha por motorista, do maior total para o menor.
def linhas
@linhas ||= registros.group_by(&:motorista_nome).map do |nome, cms|
pagos = cms.select(&:pago?)
pendentes = cms.reject(&:pago?)
{
motorista: nome,
consolidacoes: cms.size,
lancamentos: lancamentos_por_motorista[nome].to_i,
valor_total: soma(cms),
valor_pago: soma(pagos),
valor_pendente: soma(pendentes),
pagas: pagos.size,
pendentes: pendentes.size
}
end.sort_by { |l| -l[:valor_total] }
end
# Consolidações que compõem o total de UM motorista (o drill-down), da mais
# recente para a mais antiga.
def consolidacoes_de(nome)
registros.select { |cm| cm.motorista_nome == nome }
.sort_by { |cm| [cm.consolidacao.data_fim, cm.consolidacao.data_inicio] }
.reverse
end
def motoristas
linhas.map { |l| l[:motorista] }
end
def total_geral = soma(registros)
def total_pago = soma(registros.select(&:pago?))
def total_pendente = soma(registros.reject(&:pago?))
def total_consolidacoes = registros.map(&:consolidacao_id).uniq.size
def vazio? = registros.empty?
def periodo_label
"#{@inicio.strftime('%d/%m/%Y')} a #{@fim.strftime('%d/%m/%Y')}"
end
private
# As consolidações do período: as que cruzam a faixa + as que receberam
# pagamento dentro dela. Mesmo par de conjuntos do dashboard financeiro.
def ids_do_periodo(base)
intervalo = @inicio.beginning_of_day..@fim.end_of_day
cruzam = base.cruzando_periodo(@inicio, @fim).ids
pagas = ConsolidacaoMotorista.ativos
.where(pago_em: intervalo)
.where(consolidacao_id: base.select(:id))
.distinct.pluck(:consolidacao_id)
(cruzam + pagas).uniq
end
def soma(cms)
cms.sum { |cm| cm.valor_total || 0 }
end
# Lançamentos classificados por motorista (soma da quantidade — um termo com
# quantidade 5 vale 5). Uma query só para todas as consolidações do recorte,
# em vez de uma por linha da tabela.
def lancamentos_por_motorista
@lancamentos_por_motorista ||= begin
ids = registros.map(&:consolidacao_id).uniq
if ids.empty?
{}
else
# O EXISTS espelha o `ConsolidacaoMotorista.ativos` de `registros`, e por
# PAR (consolidação, motorista) — filtrar só por nome não basta: o
# mesmo motorista pode estar ativo numa consolidação do período e
# arquivado em outra, e aí os lançamentos da arquivada entravam nesta
# contagem enquanto o R$ da mesma linha os excluía. Arquivar motorista
# é reversível e NÃO apaga consolidacao_entregas (só o excluir apaga).
ConsolidacaoEntrega.where(consolidacao_id: ids,
motorista_nome: registros.map(&:motorista_nome).uniq)
.where(<<~SQL.squish)
EXISTS (SELECT 1 FROM consolidacao_motoristas cm
WHERE cm.consolidacao_id = consolidacao_entregas.consolidacao_id
AND cm.motorista_nome = consolidacao_entregas.motorista_nome
AND cm.arquivado_em IS NULL)
SQL
.group(:motorista_nome)
.sum(:quantidade)
end
end
end
end
end